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 \n". Dumper($section)
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 warn "$me setting options\n"
2773 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2775 $options{'section'} = $section if $multisection;
2776 $options{'format'} = $format;
2777 $options{'escape_function'} = $escape_function;
2778 $options{'format_function'} = sub { () } unless $unsquelched;
2779 $options{'unsquelched'} = $unsquelched;
2780 $options{'summary_page'} = $summarypage;
2781 $options{'skip_usage'} =
2782 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2783 $options{'multilocation'} = $multilocation;
2784 $options{'multisection'} = $multisection;
2786 warn "$me searching for line items\n"
2789 foreach my $line_item ( $self->_items_pkg(%options) ) {
2791 warn "$me adding line item $line_item\n"
2795 ext_description => [],
2797 $detail->{'ref'} = $line_item->{'pkgnum'};
2798 $detail->{'quantity'} = $line_item->{'quantity'};
2799 $detail->{'section'} = $section;
2800 $detail->{'description'} = &$escape_function($line_item->{'description'});
2801 if ( exists $line_item->{'ext_description'} ) {
2802 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2804 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2805 $line_item->{'amount'};
2806 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2807 $line_item->{'unit_amount'};
2808 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2810 push @detail_items, $detail;
2811 push @buf, ( [ $detail->{'description'},
2812 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2814 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2818 if ( $section->{'description'} ) {
2819 push @buf, ( ['','-----------'],
2820 [ $section->{'description'}. ' sub-total',
2821 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2830 $invoice_data{current_less_finance} =
2831 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2833 if ( $multisection && !$conf->exists('disable_previous_balance')
2834 || $conf->exists('previous_balance-summary_only') )
2836 unshift @sections, $previous_section if $pr_total;
2839 warn "$me adding taxes\n"
2842 foreach my $tax ( $self->_items_tax ) {
2844 $taxtotal += $tax->{'amount'};
2846 my $description = &$escape_function( $tax->{'description'} );
2847 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2849 if ( $multisection ) {
2851 my $money = $old_latex ? '' : $money_char;
2852 push @detail_items, {
2853 ext_description => [],
2856 description => $description,
2857 amount => $money. $amount,
2859 section => $tax_section,
2864 push @total_items, {
2865 'total_item' => $description,
2866 'total_amount' => $other_money_char. $amount,
2871 push @buf,[ $description,
2872 $money_char. $amount,
2879 $total->{'total_item'} = 'Sub-total';
2880 $total->{'total_amount'} =
2881 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2883 if ( $multisection ) {
2884 $tax_section->{'subtotal'} = $other_money_char.
2885 sprintf('%.2f', $taxtotal);
2886 $tax_section->{'pretotal'} = 'New charges sub-total '.
2887 $total->{'total_amount'};
2888 push @sections, $tax_section if $taxtotal;
2890 unshift @total_items, $total;
2893 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2895 push @buf,['','-----------'];
2896 push @buf,[( $conf->exists('disable_previous_balance')
2898 : 'Total New Charges'
2900 $money_char. sprintf("%10.2f",$self->charged) ];
2906 $item = $conf->config('previous_balance-exclude_from_total')
2907 || 'Total New Charges'
2908 if $conf->exists('previous_balance-exclude_from_total');
2909 my $amount = $self->charged +
2910 ( $conf->exists('disable_previous_balance') ||
2911 $conf->exists('previous_balance-exclude_from_total')
2915 $total->{'total_item'} = &$embolden_function($item);
2916 $total->{'total_amount'} =
2917 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2918 if ( $multisection ) {
2919 if ( $adjust_section->{'sort_weight'} ) {
2920 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2921 sprintf("%.2f", ($self->billing_balance || 0) );
2923 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2924 sprintf('%.2f', $self->charged );
2927 push @total_items, $total;
2929 push @buf,['','-----------'];
2932 sprintf( '%10.2f', $amount )
2937 unless ( $conf->exists('disable_previous_balance') ) {
2938 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2941 my $credittotal = 0;
2942 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2945 $total->{'total_item'} = &$escape_function($credit->{'description'});
2946 $credittotal += $credit->{'amount'};
2947 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2948 $adjusttotal += $credit->{'amount'};
2949 if ( $multisection ) {
2950 my $money = $old_latex ? '' : $money_char;
2951 push @detail_items, {
2952 ext_description => [],
2955 description => &$escape_function($credit->{'description'}),
2956 amount => $money. $credit->{'amount'},
2958 section => $adjust_section,
2961 push @total_items, $total;
2965 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2968 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2969 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2973 my $paymenttotal = 0;
2974 foreach my $payment ( $self->_items_payments ) {
2976 $total->{'total_item'} = &$escape_function($payment->{'description'});
2977 $paymenttotal += $payment->{'amount'};
2978 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2979 $adjusttotal += $payment->{'amount'};
2980 if ( $multisection ) {
2981 my $money = $old_latex ? '' : $money_char;
2982 push @detail_items, {
2983 ext_description => [],
2986 description => &$escape_function($payment->{'description'}),
2987 amount => $money. $payment->{'amount'},
2989 section => $adjust_section,
2992 push @total_items, $total;
2994 push @buf, [ $payment->{'description'},
2995 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2998 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3000 if ( $multisection ) {
3001 $adjust_section->{'subtotal'} = $other_money_char.
3002 sprintf('%.2f', $adjusttotal);
3003 push @sections, $adjust_section
3004 unless $adjust_section->{sort_weight};
3009 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3010 $total->{'total_amount'} =
3011 &$embolden_function(
3012 $other_money_char. sprintf('%.2f', $summarypage
3014 $self->billing_balance
3015 : $self->owed + $pr_total
3018 if ( $multisection && !$adjust_section->{sort_weight} ) {
3019 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3020 $total->{'total_amount'};
3022 push @total_items, $total;
3024 push @buf,['','-----------'];
3025 push @buf,[$self->balance_due_msg, $money_char.
3026 sprintf("%10.2f", $balance_due ) ];
3030 if ( $multisection ) {
3031 if ($conf->exists('svc_phone_sections')) {
3033 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3034 $total->{'total_amount'} =
3035 &$embolden_function(
3036 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3038 my $last_section = pop @sections;
3039 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3040 $total->{'total_amount'};
3041 push @sections, $last_section;
3043 push @sections, @$late_sections
3047 my @includelist = ();
3048 push @includelist, 'summary' if $summarypage;
3049 foreach my $include ( @includelist ) {
3051 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3054 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3056 @inc_src = $conf->config($inc_file, $agentnum);
3060 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3062 my $convert_map = $convert_maps{$format}{$include};
3064 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3065 s/--\@\]/$delimiters{$format}[1]/g;
3068 &$convert_map( $conf->config($inc_file, $agentnum) );
3072 my $inc_tt = new Text::Template (
3074 SOURCE => [ map "$_\n", @inc_src ],
3075 DELIMITERS => $delimiters{$format},
3076 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3078 unless ( $inc_tt->compile() ) {
3079 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3080 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3084 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3086 $invoice_data{$include} =~ s/\n+$//
3087 if ($format eq 'latex');
3092 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3093 /invoice_lines\((\d*)\)/;
3094 $invoice_lines += $1 || scalar(@buf);
3097 die "no invoice_lines() functions in template?"
3098 if ( $format eq 'template' && !$wasfunc );
3100 if ($format eq 'template') {
3102 if ( $invoice_lines ) {
3103 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3104 $invoice_data{'total_pages'}++
3105 if scalar(@buf) % $invoice_lines;
3108 #setup subroutine for the template
3109 sub FS::cust_bill::_template::invoice_lines {
3110 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3112 scalar(@FS::cust_bill::_template::buf)
3113 ? shift @FS::cust_bill::_template::buf
3122 push @collect, split("\n",
3123 $text_template->fill_in( HASH => \%invoice_data,
3124 PACKAGE => 'FS::cust_bill::_template'
3127 $FS::cust_bill::_template::page++;
3129 map "$_\n", @collect;
3131 warn "filling in template for invoice ". $self->invnum. "\n"
3133 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3136 $text_template->fill_in(HASH => \%invoice_data);
3140 # helper routine for generating date ranges
3141 sub _prior_month30s {
3144 [ 1, 2592000 ], # 0-30 days ago
3145 [ 2592000, 5184000 ], # 30-60 days ago
3146 [ 5184000, 7776000 ], # 60-90 days ago
3147 [ 7776000, 0 ], # 90+ days ago
3150 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3151 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3156 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3158 Returns an postscript invoice, as a scalar.
3160 Options can be passed as a hashref (recommended) or as a list of time, template
3161 and then any key/value pairs for any other options.
3163 I<time> an optional value used to control the printing of overdue messages. The
3164 default is now. It isn't the date of the invoice; that's the `_date' field.
3165 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3166 L<Time::Local> and L<Date::Parse> for conversion functions.
3168 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3175 my ($file, $lfile) = $self->print_latex(@_);
3176 my $ps = generate_ps($file);
3182 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3184 Returns an PDF invoice, as a scalar.
3186 Options can be passed as a hashref (recommended) or as a list of time, template
3187 and then any key/value pairs for any other options.
3189 I<time> an optional value used to control the printing of overdue messages. The
3190 default is now. It isn't the date of the invoice; that's the `_date' field.
3191 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3192 L<Time::Local> and L<Date::Parse> for conversion functions.
3194 I<template>, if specified, is the name of a suffix for alternate invoices.
3196 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3203 my ($file, $lfile) = $self->print_latex(@_);
3204 my $pdf = generate_pdf($file);
3210 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3212 Returns an HTML invoice, as a scalar.
3214 I<time> an optional value used to control the printing of overdue messages. The
3215 default is now. It isn't the date of the invoice; that's the `_date' field.
3216 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3217 L<Time::Local> and L<Date::Parse> for conversion functions.
3219 I<template>, if specified, is the name of a suffix for alternate invoices.
3221 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3223 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3224 when emailing the invoice as part of a multipart/related MIME email.
3232 %params = %{ shift() };
3234 $params{'time'} = shift;
3235 $params{'template'} = shift;
3236 $params{'cid'} = shift;
3239 $params{'format'} = 'html';
3241 $self->print_generic( %params );
3244 # quick subroutine for print_latex
3246 # There are ten characters that LaTeX treats as special characters, which
3247 # means that they do not simply typeset themselves:
3248 # # $ % & ~ _ ^ \ { }
3250 # TeX ignores blanks following an escaped character; if you want a blank (as
3251 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3255 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3256 $value =~ s/([<>])/\$$1\$/g;
3262 encode_entities($value);
3266 sub _html_escape_nbsp {
3267 my $value = _html_escape(shift);
3268 $value =~ s/ +/ /g;
3272 #utility methods for print_*
3274 sub _translate_old_latex_format {
3275 warn "_translate_old_latex_format called\n"
3282 if ( $line =~ /^%%Detail\s*$/ ) {
3284 push @template, q![@--!,
3285 q! foreach my $_tr_line (@detail_items) {!,
3286 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3287 q! $_tr_line->{'description'} .= !,
3288 q! "\\tabularnewline\n~~".!,
3289 q! join( "\\tabularnewline\n~~",!,
3290 q! @{$_tr_line->{'ext_description'}}!,
3294 while ( ( my $line_item_line = shift )
3295 !~ /^%%EndDetail\s*$/ ) {
3296 $line_item_line =~ s/'/\\'/g; # nice LTS
3297 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3298 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3299 push @template, " \$OUT .= '$line_item_line';";
3302 push @template, '}',
3305 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3307 push @template, '[@--',
3308 ' foreach my $_tr_line (@total_items) {';
3310 while ( ( my $total_item_line = shift )
3311 !~ /^%%EndTotalDetails\s*$/ ) {
3312 $total_item_line =~ s/'/\\'/g; # nice LTS
3313 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3314 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3315 push @template, " \$OUT .= '$total_item_line';";
3318 push @template, '}',
3322 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3323 push @template, $line;
3329 warn "$_\n" foreach @template;
3338 #check for an invoice-specific override
3339 return $self->invoice_terms if $self->invoice_terms;
3341 #check for a customer- specific override
3342 my $cust_main = $self->cust_main;
3343 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3345 #use configured default
3346 $conf->config('invoice_default_terms') || '';
3352 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3353 $duedate = $self->_date() + ( $1 * 86400 );
3360 $self->due_date ? time2str(shift, $self->due_date) : '';
3363 sub balance_due_msg {
3365 my $msg = 'Balance Due';
3366 return $msg unless $self->terms;
3367 if ( $self->due_date ) {
3368 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3369 } elsif ( $self->terms ) {
3370 $msg .= ' - '. $self->terms;
3375 sub balance_due_date {
3378 if ( $conf->exists('invoice_default_terms')
3379 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3380 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3385 =item invnum_date_pretty
3387 Returns a string with the invoice number and date, for example:
3388 "Invoice #54 (3/20/2008)"
3392 sub invnum_date_pretty {
3394 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3399 Returns a string with the date, for example: "3/20/2008"
3405 time2str($date_format, $self->_date);
3408 use vars qw(%pkg_category_cache);
3409 sub _items_sections {
3412 my $summarypage = shift;
3414 my $extra_sections = shift;
3418 my %late_subtotal = ();
3421 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3424 my $usage = $cust_bill_pkg->usage;
3426 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3427 next if ( $display->summary && $summarypage );
3429 my $section = $display->section;
3430 my $type = $display->type;
3432 $not_tax{$section} = 1
3433 unless $cust_bill_pkg->pkgnum == 0;
3435 if ( $display->post_total && !$summarypage ) {
3436 if (! $type || $type eq 'S') {
3437 $late_subtotal{$section} += $cust_bill_pkg->setup
3438 if $cust_bill_pkg->setup != 0;
3442 $late_subtotal{$section} += $cust_bill_pkg->recur
3443 if $cust_bill_pkg->recur != 0;
3446 if ($type && $type eq 'R') {
3447 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3448 if $cust_bill_pkg->recur != 0;
3451 if ($type && $type eq 'U') {
3452 $late_subtotal{$section} += $usage
3453 unless scalar(@$extra_sections);
3458 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3460 if (! $type || $type eq 'S') {
3461 $subtotal{$section} += $cust_bill_pkg->setup
3462 if $cust_bill_pkg->setup != 0;
3466 $subtotal{$section} += $cust_bill_pkg->recur
3467 if $cust_bill_pkg->recur != 0;
3470 if ($type && $type eq 'R') {
3471 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3472 if $cust_bill_pkg->recur != 0;
3475 if ($type && $type eq 'U') {
3476 $subtotal{$section} += $usage
3477 unless scalar(@$extra_sections);
3486 %pkg_category_cache = ();
3488 push @$late, map { { 'description' => &{$escape}($_),
3489 'subtotal' => $late_subtotal{$_},
3491 'sort_weight' => ( _pkg_category($_)
3492 ? _pkg_category($_)->weight
3495 ((_pkg_category($_) && _pkg_category($_)->condense)
3496 ? $self->_condense_section($format)
3500 sort _sectionsort keys %late_subtotal;
3503 if ( $summarypage ) {
3504 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3505 map { $_->categoryname } qsearch('pkg_category', {});
3506 push @sections, '' if exists($subtotal{''});
3508 @sections = keys %subtotal;
3511 my @early = map { { 'description' => &{$escape}($_),
3512 'subtotal' => $subtotal{$_},
3513 'summarized' => $not_tax{$_} ? '' : 'Y',
3514 'tax_section' => $not_tax{$_} ? '' : 'Y',
3515 'sort_weight' => ( _pkg_category($_)
3516 ? _pkg_category($_)->weight
3519 ((_pkg_category($_) && _pkg_category($_)->condense)
3520 ? $self->_condense_section($format)
3525 push @early, @$extra_sections if $extra_sections;
3527 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3531 #helper subs for above
3534 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3538 my $categoryname = shift;
3539 $pkg_category_cache{$categoryname} ||=
3540 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3543 my %condensed_format = (
3544 'label' => [ qw( Description Qty Amount ) ],
3546 sub { shift->{description} },
3547 sub { shift->{quantity} },
3548 sub { my($href, %opt) = @_;
3549 ($opt{dollar} || ''). $href->{amount};
3552 'align' => [ qw( l r r ) ],
3553 'span' => [ qw( 5 1 1 ) ], # unitprices?
3554 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3557 sub _condense_section {
3558 my ( $self, $format ) = ( shift, shift );
3560 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3561 qw( description_generator
3564 total_line_generator
3569 sub _condensed_generator_defaults {
3570 my ( $self, $format ) = ( shift, shift );
3571 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3580 sub _condensed_header_generator {
3581 my ( $self, $format ) = ( shift, shift );
3583 my ( $f, $prefix, $suffix, $separator, $column ) =
3584 _condensed_generator_defaults($format);
3586 if ($format eq 'latex') {
3587 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3588 $suffix = "\\\\\n\\hline";
3591 sub { my ($d,$a,$s,$w) = @_;
3592 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3594 } elsif ( $format eq 'html' ) {
3595 $prefix = '<th></th>';
3599 sub { my ($d,$a,$s,$w) = @_;
3600 return qq!<th align="$html_align{$a}">$d</th>!;
3608 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3610 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3613 $prefix. join($separator, @result). $suffix;
3618 sub _condensed_description_generator {
3619 my ( $self, $format ) = ( shift, shift );
3621 my ( $f, $prefix, $suffix, $separator, $column ) =
3622 _condensed_generator_defaults($format);
3624 my $money_char = '$';
3625 if ($format eq 'latex') {
3626 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3628 $separator = " & \n";
3630 sub { my ($d,$a,$s,$w) = @_;
3631 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3633 $money_char = '\\dollar';
3634 }elsif ( $format eq 'html' ) {
3635 $prefix = '"><td align="center"></td>';
3639 sub { my ($d,$a,$s,$w) = @_;
3640 return qq!<td align="$html_align{$a}">$d</td>!;
3642 #$money_char = $conf->config('money_char') || '$';
3643 $money_char = ''; # this is madness
3651 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3653 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3655 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3656 map { $f->{$_}->[$i] } qw(align span width)
3660 $prefix. join( $separator, @result ). $suffix;
3665 sub _condensed_total_generator {
3666 my ( $self, $format ) = ( shift, shift );
3668 my ( $f, $prefix, $suffix, $separator, $column ) =
3669 _condensed_generator_defaults($format);
3672 if ($format eq 'latex') {
3675 $separator = " & \n";
3677 sub { my ($d,$a,$s,$w) = @_;
3678 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3680 }elsif ( $format eq 'html' ) {
3684 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3686 sub { my ($d,$a,$s,$w) = @_;
3687 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3696 # my $r = &{$f->{fields}->[$i]}(@args);
3697 # $r .= ' Total' unless $i;
3699 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3701 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3702 map { $f->{$_}->[$i] } qw(align span width)
3706 $prefix. join( $separator, @result ). $suffix;
3711 =item total_line_generator FORMAT
3713 Returns a coderef used for generation of invoice total line items for this
3714 usage_class. FORMAT is either html or latex
3718 # should not be used: will have issues with hash element names (description vs
3719 # total_item and amount vs total_amount -- another array of functions?
3721 sub _condensed_total_line_generator {
3722 my ( $self, $format ) = ( shift, shift );
3724 my ( $f, $prefix, $suffix, $separator, $column ) =
3725 _condensed_generator_defaults($format);
3728 if ($format eq 'latex') {
3731 $separator = " & \n";
3733 sub { my ($d,$a,$s,$w) = @_;
3734 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3736 }elsif ( $format eq 'html' ) {
3740 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3742 sub { my ($d,$a,$s,$w) = @_;
3743 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3752 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3754 &{$column}( &{$f->{fields}->[$i]}(@args),
3755 map { $f->{$_}->[$i] } qw(align span width)
3759 $prefix. join( $separator, @result ). $suffix;
3764 #sub _items_extra_usage_sections {
3766 # my $escape = shift;
3768 # my %sections = ();
3770 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3771 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3773 # next unless $cust_bill_pkg->pkgnum > 0;
3775 # foreach my $section ( keys %usage_class ) {
3777 # my $usage = $cust_bill_pkg->usage($section);
3779 # next unless $usage && $usage > 0;
3781 # $sections{$section} ||= 0;
3782 # $sections{$section} += $usage;
3788 # map { { 'description' => &{$escape}($_),
3789 # 'subtotal' => $sections{$_},
3790 # 'summarized' => '',
3791 # 'tax_section' => '',
3794 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3798 sub _items_extra_usage_sections {
3807 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3808 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3809 next unless $cust_bill_pkg->pkgnum > 0;
3811 foreach my $classnum ( keys %usage_class ) {
3812 my $section = $usage_class{$classnum}->classname;
3813 $classnums{$section} = $classnum;
3815 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3816 my $amount = $detail->amount;
3817 next unless $amount && $amount > 0;
3819 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3820 $sections{$section}{amount} += $amount; #subtotal
3821 $sections{$section}{calls}++;
3822 $sections{$section}{duration} += $detail->duration;
3824 my $desc = $detail->regionname;
3825 my $description = $desc;
3826 $description = substr($desc, 0, 50). '...'
3827 if $format eq 'latex' && length($desc) > 50;
3829 $lines{$section}{$desc} ||= {
3830 description => &{$escape}($description),
3831 #pkgpart => $part_pkg->pkgpart,
3832 pkgnum => $cust_bill_pkg->pkgnum,
3837 #unit_amount => $cust_bill_pkg->unitrecur,
3838 quantity => $cust_bill_pkg->quantity,
3839 product_code => 'N/A',
3840 ext_description => [],
3843 $lines{$section}{$desc}{amount} += $amount;
3844 $lines{$section}{$desc}{calls}++;
3845 $lines{$section}{$desc}{duration} += $detail->duration;
3851 my %sectionmap = ();
3852 foreach (keys %sections) {
3853 my $usage_class = $usage_class{$classnums{$_}};
3854 $sectionmap{$_} = { 'description' => &{$escape}($_),
3855 'amount' => $sections{$_}{amount}, #subtotal
3856 'calls' => $sections{$_}{calls},
3857 'duration' => $sections{$_}{duration},
3859 'tax_section' => '',
3860 'sort_weight' => $usage_class->weight,
3861 ( $usage_class->format
3862 ? ( map { $_ => $usage_class->$_($format) }
3863 qw( description_generator header_generator total_generator total_line_generator )
3870 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3874 foreach my $section ( keys %lines ) {
3875 foreach my $line ( keys %{$lines{$section}} ) {
3876 my $l = $lines{$section}{$line};
3877 $l->{section} = $sectionmap{$section};
3878 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3879 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3884 return(\@sections, \@lines);
3890 my $end = $self->_date;
3891 my $start = $end - 2592000; # 30 days
3892 my $cust_main = $self->cust_main;
3893 my @pkgs = $cust_main->all_pkgs;
3894 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3897 foreach my $pkg ( @pkgs ) {
3898 my @h_cust_svc = $pkg->h_cust_svc($end);
3899 foreach my $h_cust_svc ( @h_cust_svc ) {
3900 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3901 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3903 my $inserted = $h_cust_svc->date_inserted;
3904 my $deleted = $h_cust_svc->date_deleted;
3905 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3907 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3909 # DID either activated or ported in; cannot be both for same DID simultaneously
3910 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3911 && (!$phone_inserted->lnp_status
3912 || $phone_inserted->lnp_status eq ''
3913 || $phone_inserted->lnp_status eq 'native')) {
3916 else { # this one not so clean, should probably move to (h_)svc_phone
3917 my $phone_portedin = qsearchs( 'h_svc_phone',
3918 { 'svcnum' => $h_cust_svc->svcnum,
3919 'lnp_status' => 'portedin' },
3920 FS::h_svc_phone->sql_h_searchs($end),
3922 $num_portedin++ if $phone_portedin;
3925 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3926 if($deleted >= $start && $deleted <= $end && $phone_deleted
3927 && (!$phone_deleted->lnp_status
3928 || $phone_deleted->lnp_status ne 'portingout')) {
3931 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3932 && $phone_deleted->lnp_status
3933 && $phone_deleted->lnp_status eq 'portingout') {
3937 # increment usage minutes
3938 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3939 foreach my $cdr ( @cdrs ) {
3940 $minutes += $cdr->billsec/60;
3943 # don't look at this service again
3944 push @seen, $h_cust_svc->svcnum;
3948 $minutes = sprintf("%d", $minutes);
3949 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3950 . "$num_deactivated Ported-Out: $num_portedout ",
3951 "Total Minutes: $minutes");
3954 sub _items_svc_phone_sections {
3963 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3964 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3966 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3967 next unless $cust_bill_pkg->pkgnum > 0;
3969 my @header = $cust_bill_pkg->details_header;
3970 next unless scalar(@header);
3972 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3974 my $phonenum = $detail->phonenum;
3975 next unless $phonenum;
3977 my $amount = $detail->amount;
3978 next unless $amount && $amount > 0;
3980 $sections{$phonenum} ||= { 'amount' => 0,
3983 'sort_weight' => -1,
3984 'phonenum' => $phonenum,
3986 $sections{$phonenum}{amount} += $amount; #subtotal
3987 $sections{$phonenum}{calls}++;
3988 $sections{$phonenum}{duration} += $detail->duration;
3990 my $desc = $detail->regionname;
3991 my $description = $desc;
3992 $description = substr($desc, 0, 50). '...'
3993 if $format eq 'latex' && length($desc) > 50;
3995 $lines{$phonenum}{$desc} ||= {
3996 description => &{$escape}($description),
3997 #pkgpart => $part_pkg->pkgpart,
4005 product_code => 'N/A',
4006 ext_description => [],
4009 $lines{$phonenum}{$desc}{amount} += $amount;
4010 $lines{$phonenum}{$desc}{calls}++;
4011 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4013 my $line = $usage_class{$detail->classnum}->classname;
4014 $sections{"$phonenum $line"} ||=
4018 'sort_weight' => $usage_class{$detail->classnum}->weight,
4019 'phonenum' => $phonenum,
4020 'header' => [ @header ],
4022 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4023 $sections{"$phonenum $line"}{calls}++;
4024 $sections{"$phonenum $line"}{duration} += $detail->duration;
4026 $lines{"$phonenum $line"}{$desc} ||= {
4027 description => &{$escape}($description),
4028 #pkgpart => $part_pkg->pkgpart,
4036 product_code => 'N/A',
4037 ext_description => [],
4040 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4041 $lines{"$phonenum $line"}{$desc}{calls}++;
4042 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4043 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4044 $detail->formatted('format' => $format);
4049 my %sectionmap = ();
4050 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4051 foreach ( keys %sections ) {
4052 my @header = @{ $sections{$_}{header} || [] };
4054 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4055 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4056 my $usage_class = $summary ? $simple : $usage_simple;
4057 my $ending = $summary ? ' usage charges' : '';
4060 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4062 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4063 'amount' => $sections{$_}{amount}, #subtotal
4064 'calls' => $sections{$_}{calls},
4065 'duration' => $sections{$_}{duration},
4067 'tax_section' => '',
4068 'phonenum' => $sections{$_}{phonenum},
4069 'sort_weight' => $sections{$_}{sort_weight},
4070 'post_total' => $summary, #inspire pagebreak
4072 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4073 qw( description_generator
4076 total_line_generator
4083 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4084 $a->{sort_weight} <=> $b->{sort_weight}
4089 foreach my $section ( keys %lines ) {
4090 foreach my $line ( keys %{$lines{$section}} ) {
4091 my $l = $lines{$section}{$line};
4092 $l->{section} = $sectionmap{$section};
4093 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4094 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4099 return(\@sections, \@lines);
4106 #my @display = scalar(@_)
4108 # : qw( _items_previous _items_pkg );
4109 # #: qw( _items_pkg );
4110 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4111 my @display = qw( _items_previous _items_pkg );
4114 foreach my $display ( @display ) {
4115 push @b, $self->$display(@_);
4120 sub _items_previous {
4122 my $cust_main = $self->cust_main;
4123 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4125 foreach ( @pr_cust_bill ) {
4126 my $date = $conf->exists('invoice_show_prior_due_date')
4127 ? 'due '. $_->due_date2str($date_format)
4128 : time2str($date_format, $_->_date);
4130 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4131 #'pkgpart' => 'N/A',
4133 'amount' => sprintf("%.2f", $_->owed),
4139 # 'description' => 'Previous Balance',
4140 # #'pkgpart' => 'N/A',
4141 # 'pkgnum' => 'N/A',
4142 # 'amount' => sprintf("%10.2f", $pr_total ),
4143 # 'ext_description' => [ map {
4144 # "Invoice ". $_->invnum.
4145 # " (". time2str("%x",$_->_date). ") ".
4146 # sprintf("%10.2f", $_->owed)
4147 # } @pr_cust_bill ],
4156 warn "$me _items_pkg searching for all package line items\n"
4159 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4161 warn "$me _items_pkg filtering line items\n"
4163 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4165 if ($options{section} && $options{section}->{condensed}) {
4167 warn "$me _items_pkg condensing section\n"
4171 local $Storable::canonical = 1;
4172 foreach ( @items ) {
4174 delete $item->{ref};
4175 delete $item->{ext_description};
4176 my $key = freeze($item);
4177 $itemshash{$key} ||= 0;
4178 $itemshash{$key} ++; # += $item->{quantity};
4180 @items = sort { $a->{description} cmp $b->{description} }
4181 map { my $i = thaw($_);
4182 $i->{quantity} = $itemshash{$_};
4184 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4190 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4197 return 0 unless $a->itemdesc cmp $b->itemdesc;
4198 return -1 if $b->itemdesc eq 'Tax';
4199 return 1 if $a->itemdesc eq 'Tax';
4200 return -1 if $b->itemdesc eq 'Other surcharges';
4201 return 1 if $a->itemdesc eq 'Other surcharges';
4202 $a->itemdesc cmp $b->itemdesc;
4207 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4208 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4211 sub _items_cust_bill_pkg {
4213 my $cust_bill_pkgs = shift;
4216 my $format = $opt{format} || '';
4217 my $escape_function = $opt{escape_function} || sub { shift };
4218 my $format_function = $opt{format_function} || '';
4219 my $unsquelched = $opt{unsquelched} || '';
4220 my $section = $opt{section}->{description} if $opt{section};
4221 my $summary_page = $opt{summary_page} || '';
4222 my $multilocation = $opt{multilocation} || '';
4223 my $multisection = $opt{multisection} || '';
4224 my $discount_show_always = 0;
4227 my ($s, $r, $u) = ( undef, undef, undef );
4228 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4231 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4234 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4235 && $conf->exists('discount-show-always'));
4237 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4238 if ( $_ && !$cust_bill_pkg->hidden ) {
4239 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4240 $_->{amount} =~ s/^\-0\.00$/0.00/;
4241 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4243 unless ( $_->{amount} == 0 && !$discount_show_always );
4248 foreach my $display ( grep { defined($section)
4249 ? $_->section eq $section
4252 #grep { !$_->summary || !$summary_page } # bunk!
4253 grep { !$_->summary || $multisection }
4254 $cust_bill_pkg->cust_bill_pkg_display
4258 warn "$me _items_cust_bill_pkg considering display item $display\n"
4261 my $type = $display->type;
4263 my $desc = $cust_bill_pkg->desc;
4264 $desc = substr($desc, 0, 50). '...'
4265 if $format eq 'latex' && length($desc) > 50;
4267 my %details_opt = ( 'format' => $format,
4268 'escape_function' => $escape_function,
4269 'format_function' => $format_function,
4272 if ( $cust_bill_pkg->pkgnum > 0 ) {
4274 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4277 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4279 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4281 warn "$me _items_cust_bill_pkg adding setup\n"
4284 my $description = $desc;
4285 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4288 unless ( $cust_pkg->part_pkg->hide_svc_detail
4289 || $cust_bill_pkg->hidden )
4292 push @d, map &{$escape_function}($_),
4293 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4294 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4296 if ( $multilocation ) {
4297 my $loc = $cust_pkg->location_label;
4298 $loc = substr($loc, 0, 50). '...'
4299 if $format eq 'latex' && length($loc) > 50;
4300 push @d, &{$escape_function}($loc);
4305 push @d, $cust_bill_pkg->details(%details_opt)
4306 if $cust_bill_pkg->recur == 0;
4308 if ( $cust_bill_pkg->hidden ) {
4309 $s->{amount} += $cust_bill_pkg->setup;
4310 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4311 push @{ $s->{ext_description} }, @d;
4314 description => $description,
4315 #pkgpart => $part_pkg->pkgpart,
4316 pkgnum => $cust_bill_pkg->pkgnum,
4317 amount => $cust_bill_pkg->setup,
4318 unit_amount => $cust_bill_pkg->unitsetup,
4319 quantity => $cust_bill_pkg->quantity,
4320 ext_description => \@d,
4326 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4327 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4328 ( !$type || $type eq 'R' || $type eq 'U' )
4332 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4335 my $is_summary = $display->summary;
4336 my $description = ($is_summary && $type && $type eq 'U')
4337 ? "Usage charges" : $desc;
4339 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4340 " - ". time2str($date_format, $cust_bill_pkg->edate).
4342 unless $conf->exists('disable_line_item_date_ranges');
4346 #at least until cust_bill_pkg has "past" ranges in addition to
4347 #the "future" sdate/edate ones... see #3032
4348 my @dates = ( $self->_date );
4349 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4350 push @dates, $prev->sdate if $prev;
4351 push @dates, undef if !$prev;
4353 unless ( $cust_pkg->part_pkg->hide_svc_detail
4354 || $cust_bill_pkg->itemdesc
4355 || $cust_bill_pkg->hidden
4356 || $is_summary && $type && $type eq 'U' )
4359 warn "$me _items_cust_bill_pkg adding service details\n"
4362 push @d, map &{$escape_function}($_),
4363 $cust_pkg->h_labels_short(@dates, 'I')
4364 #$cust_bill_pkg->edate,
4365 #$cust_bill_pkg->sdate)
4366 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4368 warn "$me _items_cust_bill_pkg done adding service details\n"
4371 if ( $multilocation ) {
4372 my $loc = $cust_pkg->location_label;
4373 $loc = substr($loc, 0, 50). '...'
4374 if $format eq 'latex' && length($loc) > 50;
4375 push @d, &{$escape_function}($loc);
4380 warn "$me _items_cust_bill_pkg adding details\n"
4383 push @d, $cust_bill_pkg->details(%details_opt)
4384 unless ($is_summary || $type && $type eq 'R');
4386 warn "$me _items_cust_bill_pkg calculating amount\n"
4391 $amount = $cust_bill_pkg->recur;
4392 }elsif($type eq 'R') {
4393 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4394 }elsif($type eq 'U') {
4395 $amount = $cust_bill_pkg->usage;
4398 if ( !$type || $type eq 'R' ) {
4400 warn "$me _items_cust_bill_pkg adding recur\n"
4403 if ( $cust_bill_pkg->hidden ) {
4404 $r->{amount} += $amount;
4405 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4406 push @{ $r->{ext_description} }, @d;
4409 description => $description,
4410 #pkgpart => $part_pkg->pkgpart,
4411 pkgnum => $cust_bill_pkg->pkgnum,
4413 unit_amount => $cust_bill_pkg->unitrecur,
4414 quantity => $cust_bill_pkg->quantity,
4415 ext_description => \@d,
4419 } else { # $type eq 'U'
4421 warn "$me _items_cust_bill_pkg adding usage\n"
4424 if ( $cust_bill_pkg->hidden ) {
4425 $u->{amount} += $amount;
4426 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4427 push @{ $u->{ext_description} }, @d;
4430 description => $description,
4431 #pkgpart => $part_pkg->pkgpart,
4432 pkgnum => $cust_bill_pkg->pkgnum,
4434 unit_amount => $cust_bill_pkg->unitrecur,
4435 quantity => $cust_bill_pkg->quantity,
4436 ext_description => \@d,
4442 } # recurring or usage with recurring charge
4444 } else { #pkgnum tax or one-shot line item (??)
4446 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4449 if ( $cust_bill_pkg->setup != 0 ) {
4451 'description' => $desc,
4452 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4455 if ( $cust_bill_pkg->recur != 0 ) {
4457 'description' => "$desc (".
4458 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4459 time2str($date_format, $cust_bill_pkg->edate). ')',
4460 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4470 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4473 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4475 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4476 $_->{amount} =~ s/^\-0\.00$/0.00/;
4477 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4479 unless ( $_->{amount} == 0 && !$discount_show_always );
4487 sub _items_credits {
4488 my( $self, %opt ) = @_;
4489 my $trim_len = $opt{'trim_len'} || 60;
4493 foreach ( $self->cust_credited ) {
4495 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4497 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4498 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4499 $reason = " ($reason) " if $reason;
4502 #'description' => 'Credit ref\#'. $_->crednum.
4503 # " (". time2str("%x",$_->cust_credit->_date) .")".
4505 'description' => 'Credit applied '.
4506 time2str($date_format,$_->cust_credit->_date). $reason,
4507 'amount' => sprintf("%.2f",$_->amount),
4515 sub _items_payments {
4519 #get & print payments
4520 foreach ( $self->cust_bill_pay ) {
4522 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4525 'description' => "Payment received ".
4526 time2str($date_format,$_->cust_pay->_date ),
4527 'amount' => sprintf("%.2f", $_->amount )
4535 =item call_details [ OPTION => VALUE ... ]
4537 Returns an array of CSV strings representing the call details for this invoice
4538 The only option available is the boolean prepend_billed_number
4543 my ($self, %opt) = @_;
4545 my $format_function = sub { shift };
4547 if ($opt{prepend_billed_number}) {
4548 $format_function = sub {
4552 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4557 my @details = map { $_->details( 'format_function' => $format_function,
4558 'escape_function' => sub{ return() },
4562 $self->cust_bill_pkg;
4563 my $header = $details[0];
4564 ( $header, grep { $_ ne $header } @details );
4574 =item process_reprint
4578 sub process_reprint {
4579 process_re_X('print', @_);
4582 =item process_reemail
4586 sub process_reemail {
4587 process_re_X('email', @_);
4595 process_re_X('fax', @_);
4603 process_re_X('ftp', @_);
4610 sub process_respool {
4611 process_re_X('spool', @_);
4614 use Storable qw(thaw);
4618 my( $method, $job ) = ( shift, shift );
4619 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4621 my $param = thaw(decode_base64(shift));
4622 warn Dumper($param) if $DEBUG;
4633 my($method, $job, %param ) = @_;
4635 warn "re_X $method for job $job with param:\n".
4636 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4639 #some false laziness w/search/cust_bill.html
4641 my $orderby = 'ORDER BY cust_bill._date';
4643 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4645 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4647 my @cust_bill = qsearch( {
4648 #'select' => "cust_bill.*",
4649 'table' => 'cust_bill',
4650 'addl_from' => $addl_from,
4652 'extra_sql' => $extra_sql,
4653 'order_by' => $orderby,
4657 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4659 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4662 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4663 foreach my $cust_bill ( @cust_bill ) {
4664 $cust_bill->$method();
4666 if ( $job ) { #progressbar foo
4668 if ( time - $min_sec > $last ) {
4669 my $error = $job->update_statustext(
4670 int( 100 * $num / scalar(@cust_bill) )
4672 die $error if $error;
4683 =head1 CLASS METHODS
4689 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4694 my ($class, $start, $end) = @_;
4696 $class->paid_sql($start, $end). ' - '.
4697 $class->credited_sql($start, $end);
4702 Returns an SQL fragment to retreive the net amount (charged minus credited).
4707 my ($class, $start, $end) = @_;
4708 'charged - '. $class->credited_sql($start, $end);
4713 Returns an SQL fragment to retreive the amount paid against this invoice.
4718 my ($class, $start, $end) = @_;
4719 $start &&= "AND cust_bill_pay._date <= $start";
4720 $end &&= "AND cust_bill_pay._date > $end";
4721 $start = '' unless defined($start);
4722 $end = '' unless defined($end);
4723 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4724 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4729 Returns an SQL fragment to retreive the amount credited against this invoice.
4734 my ($class, $start, $end) = @_;
4735 $start &&= "AND cust_credit_bill._date <= $start";
4736 $end &&= "AND cust_credit_bill._date > $end";
4737 $start = '' unless defined($start);
4738 $end = '' unless defined($end);
4739 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4740 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4745 Returns an SQL fragment to retrieve the due date of an invoice.
4746 Currently only supported on PostgreSQL.
4754 cust_bill.invoice_terms,
4755 cust_main.invoice_terms,
4756 \''.($conf->config('invoice_default_terms') || '').'\'
4757 ), E\'Net (\\\\d+)\'
4759 ) * 86400 + cust_bill._date'
4762 =item search_sql_where HASHREF
4764 Class method which returns an SQL WHERE fragment to search for parameters
4765 specified in HASHREF. Valid parameters are
4771 List reference of start date, end date, as UNIX timestamps.
4781 List reference of charged limits (exclusive).
4785 List reference of charged limits (exclusive).
4789 flag, return open invoices only
4793 flag, return net invoices only
4797 =item newest_percust
4801 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4805 sub search_sql_where {
4806 my($class, $param) = @_;
4808 warn "$me search_sql_where called with params: \n".
4809 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4815 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4816 push @search, "cust_main.agentnum = $1";
4820 if ( $param->{_date} ) {
4821 my($beginning, $ending) = @{$param->{_date}};
4823 push @search, "cust_bill._date >= $beginning",
4824 "cust_bill._date < $ending";
4828 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4829 push @search, "cust_bill.invnum >= $1";
4831 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4832 push @search, "cust_bill.invnum <= $1";
4836 if ( $param->{charged} ) {
4837 my @charged = ref($param->{charged})
4838 ? @{ $param->{charged} }
4839 : ($param->{charged});
4841 push @search, map { s/^charged/cust_bill.charged/; $_; }
4845 my $owed_sql = FS::cust_bill->owed_sql;
4848 if ( $param->{owed} ) {
4849 my @owed = ref($param->{owed})
4850 ? @{ $param->{owed} }
4852 push @search, map { s/^owed/$owed_sql/; $_; }
4857 push @search, "0 != $owed_sql"
4858 if $param->{'open'};
4859 push @search, '0 != '. FS::cust_bill->net_sql
4863 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4864 if $param->{'days'};
4867 if ( $param->{'newest_percust'} ) {
4869 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4870 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4872 my @newest_where = map { my $x = $_;
4873 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4876 grep ! /^cust_main./, @search;
4877 my $newest_where = scalar(@newest_where)
4878 ? ' AND '. join(' AND ', @newest_where)
4882 push @search, "cust_bill._date = (
4883 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4884 WHERE newest_cust_bill.custnum = cust_bill.custnum
4890 #agent virtualization
4891 my $curuser = $FS::CurrentUser::CurrentUser;
4892 if ( $curuser->username eq 'fs_queue'
4893 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4895 my $newuser = qsearchs('access_user', {
4896 'username' => $username,
4900 $curuser = $newuser;
4902 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4905 push @search, $curuser->agentnums_sql;
4907 join(' AND ', @search );
4919 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4920 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base