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 #create the template
2221 my $template = $params{template} ? $params{template} : $self->_agent_template;
2222 my $templatefile = "invoice_$format";
2223 $templatefile .= "_$template"
2224 if length($template);
2225 my @invoice_template = map "$_\n", $conf->config($templatefile)
2226 or die "cannot load config data $templatefile";
2229 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2230 #change this to a die when the old code is removed
2231 warn "old-style invoice template $templatefile; ".
2232 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2233 $old_latex = 'true';
2234 @invoice_template = _translate_old_latex_format(@invoice_template);
2237 my $text_template = new Text::Template(
2239 SOURCE => \@invoice_template,
2240 DELIMITERS => $delimiters{$format},
2243 $text_template->compile()
2244 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2247 # additional substitution could possibly cause breakage in existing templates
2248 my %convert_maps = (
2250 'notes' => sub { map "$_", @_ },
2251 'footer' => sub { map "$_", @_ },
2252 'smallfooter' => sub { map "$_", @_ },
2253 'returnaddress' => sub { map "$_", @_ },
2254 'coupon' => sub { map "$_", @_ },
2255 'summary' => sub { map "$_", @_ },
2261 s/%%(.*)$/<!-- $1 -->/g;
2262 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2263 s/\\begin\{enumerate\}/<ol>/g;
2265 s/\\end\{enumerate\}/<\/ol>/g;
2266 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2275 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2277 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2282 s/\\\\\*?\s*$/<BR>/;
2283 s/\\hyphenation\{[\w\s\-]+}//;
2288 'coupon' => sub { "" },
2289 'summary' => sub { "" },
2296 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2297 s/\\begin\{enumerate\}//g;
2299 s/\\end\{enumerate\}//g;
2300 s/\\textbf\{(.*)\}/$1/g;
2307 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2309 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2314 s/\\\\\*?\s*$/\n/; # dubious
2315 s/\\hyphenation\{[\w\s\-]+}//;
2319 'coupon' => sub { "" },
2320 'summary' => sub { "" },
2325 # hashes for differing output formats
2326 my %nbsps = ( 'latex' => '~',
2327 'html' => '', # '&nbps;' would be nice
2328 'template' => '', # not used
2330 my $nbsp = $nbsps{$format};
2332 my %escape_functions = ( 'latex' => \&_latex_escape,
2333 'html' => \&_html_escape_nbsp,#\&encode_entities,
2334 'template' => sub { shift },
2336 my $escape_function = $escape_functions{$format};
2337 my $escape_function_nonbsp = ($format eq 'html')
2338 ? \&_html_escape : $escape_function;
2340 my %date_formats = ( 'latex' => $date_format_long,
2341 'html' => $date_format_long,
2344 $date_formats{'html'} =~ s/ / /g;
2346 my $date_format = $date_formats{$format};
2348 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2350 'html' => sub { return '<b>'. shift(). '</b>'
2352 'template' => sub { shift },
2354 my $embolden_function = $embolden_functions{$format};
2357 # generate template variables
2360 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2364 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2370 $returnaddress = join("\n",
2371 $conf->config_orbase("invoice_${format}returnaddress", $template)
2374 } elsif ( grep /\S/,
2375 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2377 my $convert_map = $convert_maps{$format}{'returnaddress'};
2380 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2385 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2387 my $convert_map = $convert_maps{$format}{'returnaddress'};
2388 $returnaddress = join( "\n", &$convert_map(
2389 map { s/( {2,})/'~' x length($1)/eg;
2393 ( $conf->config('company_name', $self->cust_main->agentnum),
2394 $conf->config('company_address', $self->cust_main->agentnum),
2401 my $warning = "Couldn't find a return address; ".
2402 "do you need to set the company_address configuration value?";
2404 $returnaddress = $nbsp;
2405 #$returnaddress = $warning;
2409 my $agentnum = $self->cust_main->agentnum;
2411 my %invoice_data = (
2414 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2415 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2416 'returnaddress' => $returnaddress,
2417 'agent' => &$escape_function($cust_main->agent->agent),
2420 'invnum' => $self->invnum,
2421 'date' => time2str($date_format, $self->_date),
2422 'today' => time2str($date_format_long, $today),
2423 'terms' => $self->terms,
2424 'template' => $template, #params{'template'},
2425 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2426 'current_charges' => sprintf("%.2f", $self->charged),
2427 'duedate' => $self->due_date2str($rdate_format), #date_format?
2430 'custnum' => $cust_main->display_custnum,
2431 'agent_custid' => &$escape_function($cust_main->agent_custid),
2432 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2433 payname company address1 address2 city state zip fax
2437 'ship_enable' => $conf->exists('invoice-ship_address'),
2438 'unitprices' => $conf->exists('invoice-unitprice'),
2439 'smallernotes' => $conf->exists('invoice-smallernotes'),
2440 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2441 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2443 #layout info -- would be fancy to calc some of this and bury the template
2445 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2446 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2447 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2448 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2449 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2450 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2451 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2452 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2453 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2454 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2456 # better hang on to conf_dir for a while (for old templates)
2457 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2459 #these are only used when doing paged plaintext
2465 $invoice_data{finance_section} = '';
2466 if ( $conf->config('finance_pkgclass') ) {
2468 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2469 $invoice_data{finance_section} = $pkg_class->categoryname;
2471 $invoice_data{finance_amount} = '0.00';
2472 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2474 my $countrydefault = $conf->config('countrydefault') || 'US';
2475 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2476 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2477 my $method = $prefix.$_;
2478 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2480 $invoice_data{'ship_country'} = ''
2481 if ( $invoice_data{'ship_country'} eq $countrydefault );
2483 $invoice_data{'cid'} = $params{'cid'}
2486 if ( $cust_main->country eq $countrydefault ) {
2487 $invoice_data{'country'} = '';
2489 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2493 $invoice_data{'address'} = \@address;
2495 $cust_main->payname.
2496 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2497 ? " (P.O. #". $cust_main->payinfo. ")"
2501 push @address, $cust_main->company
2502 if $cust_main->company;
2503 push @address, $cust_main->address1;
2504 push @address, $cust_main->address2
2505 if $cust_main->address2;
2507 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2508 push @address, $invoice_data{'country'}
2509 if $invoice_data{'country'};
2511 while (scalar(@address) < 5);
2513 $invoice_data{'logo_file'} = $params{'logo_file'}
2514 if $params{'logo_file'};
2516 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2517 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2518 #my $balance_due = $self->owed + $pr_total - $cr_total;
2519 my $balance_due = $self->owed + $pr_total;
2520 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2521 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2522 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2523 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2525 my $summarypage = '';
2526 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2529 $invoice_data{'summarypage'} = $summarypage;
2531 #do variable substitution in notes, footer, smallfooter
2532 foreach my $include (qw( notes footer smallfooter coupon )) {
2534 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2537 if ( $conf->exists($inc_file, $agentnum)
2538 && length( $conf->config($inc_file, $agentnum) ) ) {
2540 @inc_src = $conf->config($inc_file, $agentnum);
2544 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2546 my $convert_map = $convert_maps{$format}{$include};
2548 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2549 s/--\@\]/$delimiters{$format}[1]/g;
2552 &$convert_map( $conf->config($inc_file, $agentnum) );
2556 my $inc_tt = new Text::Template (
2558 SOURCE => [ map "$_\n", @inc_src ],
2559 DELIMITERS => $delimiters{$format},
2560 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2562 unless ( $inc_tt->compile() ) {
2563 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2564 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2568 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2570 $invoice_data{$include} =~ s/\n+$//
2571 if ($format eq 'latex');
2574 $invoice_data{'po_line'} =
2575 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2576 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2579 my %money_chars = ( 'latex' => '',
2580 'html' => $conf->config('money_char') || '$',
2583 my $money_char = $money_chars{$format};
2585 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2586 'html' => $conf->config('money_char') || '$',
2589 my $other_money_char = $other_money_chars{$format};
2590 $invoice_data{'dollar'} = $other_money_char;
2592 my @detail_items = ();
2593 my @total_items = ();
2597 $invoice_data{'detail_items'} = \@detail_items;
2598 $invoice_data{'total_items'} = \@total_items;
2599 $invoice_data{'buf'} = \@buf;
2600 $invoice_data{'sections'} = \@sections;
2602 my $previous_section = { 'description' => 'Previous Charges',
2603 'subtotal' => $other_money_char.
2604 sprintf('%.2f', $pr_total),
2605 'summarized' => $summarypage ? 'Y' : '',
2607 $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '.
2608 join(' / ', map { $cust_main->balance_date_range(@$_) }
2609 $self->_prior_month30s
2611 if $conf->exists('invoice_include_aging');
2614 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2615 'subtotal' => $taxtotal, # adjusted below
2616 'summarized' => $summarypage ? 'Y' : '',
2618 my $tax_weight = _pkg_category($tax_section->{description})
2619 ? _pkg_category($tax_section->{description})->weight
2621 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2622 $tax_section->{'sort_weight'} = $tax_weight;
2625 my $adjusttotal = 0;
2626 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2627 'subtotal' => 0, # adjusted below
2628 'summarized' => $summarypage ? 'Y' : '',
2630 my $adjust_weight = _pkg_category($adjust_section->{description})
2631 ? _pkg_category($adjust_section->{description})->weight
2633 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2634 $adjust_section->{'sort_weight'} = $adjust_weight;
2636 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2637 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2638 $invoice_data{'multisection'} = $multisection;
2639 my $late_sections = [];
2640 my $extra_sections = [];
2641 my $extra_lines = ();
2642 if ( $multisection ) {
2643 ($extra_sections, $extra_lines) =
2644 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2645 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2647 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2649 push @detail_items, @$extra_lines if $extra_lines;
2651 $self->_items_sections( $late_sections, # this could stand a refactor
2653 $escape_function_nonbsp,
2657 if ($conf->exists('svc_phone_sections')) {
2658 my ($phone_sections, $phone_lines) =
2659 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2660 push @{$late_sections}, @$phone_sections;
2661 push @detail_items, @$phone_lines;
2664 push @sections, { 'description' => '', 'subtotal' => '' };
2667 unless ( $conf->exists('disable_previous_balance')
2668 || $conf->exists('previous_balance-summary_only')
2672 foreach my $line_item ( $self->_items_previous ) {
2675 ext_description => [],
2677 $detail->{'ref'} = $line_item->{'pkgnum'};
2678 $detail->{'quantity'} = 1;
2679 $detail->{'section'} = $previous_section;
2680 $detail->{'description'} = &$escape_function($line_item->{'description'});
2681 if ( exists $line_item->{'ext_description'} ) {
2682 @{$detail->{'ext_description'}} = map {
2683 &$escape_function($_);
2684 } @{$line_item->{'ext_description'}};
2686 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2687 $line_item->{'amount'};
2688 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2690 push @detail_items, $detail;
2691 push @buf, [ $detail->{'description'},
2692 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2698 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2699 push @buf, ['','-----------'];
2700 push @buf, [ 'Total Previous Balance',
2701 $money_char. sprintf("%10.2f", $pr_total) ];
2705 if ( $conf->exists('svc_phone-did-summary') ) {
2706 my ($didsummary,$minutes) = $self->_did_summary;
2707 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2709 { 'description' => $didsummary_desc,
2710 'ext_description' => [ $didsummary, $minutes ],
2715 foreach my $section (@sections, @$late_sections) {
2717 # begin some normalization
2718 $section->{'subtotal'} = $section->{'amount'}
2720 && !exists($section->{subtotal})
2721 && exists($section->{amount});
2723 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2724 if ( $invoice_data{finance_section} &&
2725 $section->{'description'} eq $invoice_data{finance_section} );
2727 $section->{'subtotal'} = $other_money_char.
2728 sprintf('%.2f', $section->{'subtotal'})
2731 # continue some normalization
2732 $section->{'amount'} = $section->{'subtotal'}
2736 if ( $section->{'description'} ) {
2737 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2742 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2744 $options{'section'} = $section if $multisection;
2745 $options{'format'} = $format;
2746 $options{'escape_function'} = $escape_function;
2747 $options{'format_function'} = sub { () } unless $unsquelched;
2748 $options{'unsquelched'} = $unsquelched;
2749 $options{'summary_page'} = $summarypage;
2750 $options{'skip_usage'} =
2751 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2752 $options{'multilocation'} = $multilocation;
2753 $options{'multisection'} = $multisection;
2755 foreach my $line_item ( $self->_items_pkg(%options) ) {
2757 ext_description => [],
2759 $detail->{'ref'} = $line_item->{'pkgnum'};
2760 $detail->{'quantity'} = $line_item->{'quantity'};
2761 $detail->{'section'} = $section;
2762 $detail->{'description'} = &$escape_function($line_item->{'description'});
2763 if ( exists $line_item->{'ext_description'} ) {
2764 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2766 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2767 $line_item->{'amount'};
2768 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2769 $line_item->{'unit_amount'};
2770 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2772 push @detail_items, $detail;
2773 push @buf, ( [ $detail->{'description'},
2774 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2776 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2780 if ( $section->{'description'} ) {
2781 push @buf, ( ['','-----------'],
2782 [ $section->{'description'}. ' sub-total',
2783 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2792 $invoice_data{current_less_finance} =
2793 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2795 if ( $multisection && !$conf->exists('disable_previous_balance')
2796 || $conf->exists('previous_balance-summary_only') )
2798 unshift @sections, $previous_section if $pr_total;
2801 foreach my $tax ( $self->_items_tax ) {
2803 $taxtotal += $tax->{'amount'};
2805 my $description = &$escape_function( $tax->{'description'} );
2806 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2808 if ( $multisection ) {
2810 my $money = $old_latex ? '' : $money_char;
2811 push @detail_items, {
2812 ext_description => [],
2815 description => $description,
2816 amount => $money. $amount,
2818 section => $tax_section,
2823 push @total_items, {
2824 'total_item' => $description,
2825 'total_amount' => $other_money_char. $amount,
2830 push @buf,[ $description,
2831 $money_char. $amount,
2838 $total->{'total_item'} = 'Sub-total';
2839 $total->{'total_amount'} =
2840 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2842 if ( $multisection ) {
2843 $tax_section->{'subtotal'} = $other_money_char.
2844 sprintf('%.2f', $taxtotal);
2845 $tax_section->{'pretotal'} = 'New charges sub-total '.
2846 $total->{'total_amount'};
2847 push @sections, $tax_section if $taxtotal;
2849 unshift @total_items, $total;
2852 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2854 push @buf,['','-----------'];
2855 push @buf,[( $conf->exists('disable_previous_balance')
2857 : 'Total New Charges'
2859 $money_char. sprintf("%10.2f",$self->charged) ];
2865 $item = $conf->config('previous_balance-exclude_from_total')
2866 || 'Total New Charges'
2867 if $conf->exists('previous_balance-exclude_from_total');
2868 my $amount = $self->charged +
2869 ( $conf->exists('disable_previous_balance') ||
2870 $conf->exists('previous_balance-exclude_from_total')
2874 $total->{'total_item'} = &$embolden_function($item);
2875 $total->{'total_amount'} =
2876 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
2877 if ( $multisection ) {
2878 if ( $adjust_section->{'sort_weight'} ) {
2879 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2880 sprintf("%.2f", ($self->billing_balance || 0) );
2882 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2883 sprintf('%.2f', $self->charged );
2886 push @total_items, $total;
2888 push @buf,['','-----------'];
2891 sprintf( '%10.2f', $amount )
2896 unless ( $conf->exists('disable_previous_balance') ) {
2897 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2900 my $credittotal = 0;
2901 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2904 $total->{'total_item'} = &$escape_function($credit->{'description'});
2905 $credittotal += $credit->{'amount'};
2906 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2907 $adjusttotal += $credit->{'amount'};
2908 if ( $multisection ) {
2909 my $money = $old_latex ? '' : $money_char;
2910 push @detail_items, {
2911 ext_description => [],
2914 description => &$escape_function($credit->{'description'}),
2915 amount => $money. $credit->{'amount'},
2917 section => $adjust_section,
2920 push @total_items, $total;
2924 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2927 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2928 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2932 my $paymenttotal = 0;
2933 foreach my $payment ( $self->_items_payments ) {
2935 $total->{'total_item'} = &$escape_function($payment->{'description'});
2936 $paymenttotal += $payment->{'amount'};
2937 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2938 $adjusttotal += $payment->{'amount'};
2939 if ( $multisection ) {
2940 my $money = $old_latex ? '' : $money_char;
2941 push @detail_items, {
2942 ext_description => [],
2945 description => &$escape_function($payment->{'description'}),
2946 amount => $money. $payment->{'amount'},
2948 section => $adjust_section,
2951 push @total_items, $total;
2953 push @buf, [ $payment->{'description'},
2954 $money_char. sprintf("%10.2f", $payment->{'amount'}),
2957 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2959 if ( $multisection ) {
2960 $adjust_section->{'subtotal'} = $other_money_char.
2961 sprintf('%.2f', $adjusttotal);
2962 push @sections, $adjust_section
2963 unless $adjust_section->{sort_weight};
2968 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2969 $total->{'total_amount'} =
2970 &$embolden_function(
2971 $other_money_char. sprintf('%.2f', $summarypage
2973 $self->billing_balance
2974 : $self->owed + $pr_total
2977 if ( $multisection && !$adjust_section->{sort_weight} ) {
2978 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2979 $total->{'total_amount'};
2981 push @total_items, $total;
2983 push @buf,['','-----------'];
2984 push @buf,[$self->balance_due_msg, $money_char.
2985 sprintf("%10.2f", $balance_due ) ];
2989 if ( $multisection ) {
2990 if ($conf->exists('svc_phone_sections')) {
2992 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2993 $total->{'total_amount'} =
2994 &$embolden_function(
2995 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
2997 my $last_section = pop @sections;
2998 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
2999 $total->{'total_amount'};
3000 push @sections, $last_section;
3002 push @sections, @$late_sections
3006 my @includelist = ();
3007 push @includelist, 'summary' if $summarypage;
3008 foreach my $include ( @includelist ) {
3010 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3013 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3015 @inc_src = $conf->config($inc_file, $agentnum);
3019 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3021 my $convert_map = $convert_maps{$format}{$include};
3023 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3024 s/--\@\]/$delimiters{$format}[1]/g;
3027 &$convert_map( $conf->config($inc_file, $agentnum) );
3031 my $inc_tt = new Text::Template (
3033 SOURCE => [ map "$_\n", @inc_src ],
3034 DELIMITERS => $delimiters{$format},
3035 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3037 unless ( $inc_tt->compile() ) {
3038 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3039 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3043 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3045 $invoice_data{$include} =~ s/\n+$//
3046 if ($format eq 'latex');
3051 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3052 /invoice_lines\((\d*)\)/;
3053 $invoice_lines += $1 || scalar(@buf);
3056 die "no invoice_lines() functions in template?"
3057 if ( $format eq 'template' && !$wasfunc );
3059 if ($format eq 'template') {
3061 if ( $invoice_lines ) {
3062 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3063 $invoice_data{'total_pages'}++
3064 if scalar(@buf) % $invoice_lines;
3067 #setup subroutine for the template
3068 sub FS::cust_bill::_template::invoice_lines {
3069 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3071 scalar(@FS::cust_bill::_template::buf)
3072 ? shift @FS::cust_bill::_template::buf
3081 push @collect, split("\n",
3082 $text_template->fill_in( HASH => \%invoice_data,
3083 PACKAGE => 'FS::cust_bill::_template'
3086 $FS::cust_bill::_template::page++;
3088 map "$_\n", @collect;
3090 warn "filling in template for invoice ". $self->invnum. "\n"
3092 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3095 $text_template->fill_in(HASH => \%invoice_data);
3099 # helper routine for generating date ranges
3100 sub _prior_month30s {
3103 [ 1, 2592000 ], # 0-30 days ago
3104 [ 2592000, 5184000 ], # 30-60 days ago
3105 [ 5184000, 7776000 ], # 60-90 days ago
3106 [ 7776000, 0 ], # 90+ days ago
3109 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3110 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3115 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3117 Returns an postscript invoice, as a scalar.
3119 Options can be passed as a hashref (recommended) or as a list of time, template
3120 and then any key/value pairs for any other options.
3122 I<time> an optional value used to control the printing of overdue messages. The
3123 default is now. It isn't the date of the invoice; that's the `_date' field.
3124 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3125 L<Time::Local> and L<Date::Parse> for conversion functions.
3127 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3134 my ($file, $lfile) = $self->print_latex(@_);
3135 my $ps = generate_ps($file);
3141 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3143 Returns an PDF invoice, as a scalar.
3145 Options can be passed as a hashref (recommended) or as a list of time, template
3146 and then any key/value pairs for any other options.
3148 I<time> an optional value used to control the printing of overdue messages. The
3149 default is now. It isn't the date of the invoice; that's the `_date' field.
3150 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3151 L<Time::Local> and L<Date::Parse> for conversion functions.
3153 I<template>, if specified, is the name of a suffix for alternate invoices.
3155 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3162 my ($file, $lfile) = $self->print_latex(@_);
3163 my $pdf = generate_pdf($file);
3169 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3171 Returns an HTML invoice, as a scalar.
3173 I<time> an optional value used to control the printing of overdue messages. The
3174 default is now. It isn't the date of the invoice; that's the `_date' field.
3175 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3176 L<Time::Local> and L<Date::Parse> for conversion functions.
3178 I<template>, if specified, is the name of a suffix for alternate invoices.
3180 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3182 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3183 when emailing the invoice as part of a multipart/related MIME email.
3191 %params = %{ shift() };
3193 $params{'time'} = shift;
3194 $params{'template'} = shift;
3195 $params{'cid'} = shift;
3198 $params{'format'} = 'html';
3200 $self->print_generic( %params );
3203 # quick subroutine for print_latex
3205 # There are ten characters that LaTeX treats as special characters, which
3206 # means that they do not simply typeset themselves:
3207 # # $ % & ~ _ ^ \ { }
3209 # TeX ignores blanks following an escaped character; if you want a blank (as
3210 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3214 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3215 $value =~ s/([<>])/\$$1\$/g;
3221 encode_entities($value);
3225 sub _html_escape_nbsp {
3226 my $value = _html_escape(shift);
3227 $value =~ s/ +/ /g;
3231 #utility methods for print_*
3233 sub _translate_old_latex_format {
3234 warn "_translate_old_latex_format called\n"
3241 if ( $line =~ /^%%Detail\s*$/ ) {
3243 push @template, q![@--!,
3244 q! foreach my $_tr_line (@detail_items) {!,
3245 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3246 q! $_tr_line->{'description'} .= !,
3247 q! "\\tabularnewline\n~~".!,
3248 q! join( "\\tabularnewline\n~~",!,
3249 q! @{$_tr_line->{'ext_description'}}!,
3253 while ( ( my $line_item_line = shift )
3254 !~ /^%%EndDetail\s*$/ ) {
3255 $line_item_line =~ s/'/\\'/g; # nice LTS
3256 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3257 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3258 push @template, " \$OUT .= '$line_item_line';";
3261 push @template, '}',
3264 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3266 push @template, '[@--',
3267 ' foreach my $_tr_line (@total_items) {';
3269 while ( ( my $total_item_line = shift )
3270 !~ /^%%EndTotalDetails\s*$/ ) {
3271 $total_item_line =~ s/'/\\'/g; # nice LTS
3272 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3273 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3274 push @template, " \$OUT .= '$total_item_line';";
3277 push @template, '}',
3281 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3282 push @template, $line;
3288 warn "$_\n" foreach @template;
3297 #check for an invoice-specific override
3298 return $self->invoice_terms if $self->invoice_terms;
3300 #check for a customer- specific override
3301 my $cust_main = $self->cust_main;
3302 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3304 #use configured default
3305 $conf->config('invoice_default_terms') || '';
3311 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3312 $duedate = $self->_date() + ( $1 * 86400 );
3319 $self->due_date ? time2str(shift, $self->due_date) : '';
3322 sub balance_due_msg {
3324 my $msg = 'Balance Due';
3325 return $msg unless $self->terms;
3326 if ( $self->due_date ) {
3327 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3328 } elsif ( $self->terms ) {
3329 $msg .= ' - '. $self->terms;
3334 sub balance_due_date {
3337 if ( $conf->exists('invoice_default_terms')
3338 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3339 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3344 =item invnum_date_pretty
3346 Returns a string with the invoice number and date, for example:
3347 "Invoice #54 (3/20/2008)"
3351 sub invnum_date_pretty {
3353 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3358 Returns a string with the date, for example: "3/20/2008"
3364 time2str($date_format, $self->_date);
3367 use vars qw(%pkg_category_cache);
3368 sub _items_sections {
3371 my $summarypage = shift;
3373 my $extra_sections = shift;
3377 my %late_subtotal = ();
3380 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3383 my $usage = $cust_bill_pkg->usage;
3385 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3386 next if ( $display->summary && $summarypage );
3388 my $section = $display->section;
3389 my $type = $display->type;
3391 $not_tax{$section} = 1
3392 unless $cust_bill_pkg->pkgnum == 0;
3394 if ( $display->post_total && !$summarypage ) {
3395 if (! $type || $type eq 'S') {
3396 $late_subtotal{$section} += $cust_bill_pkg->setup
3397 if $cust_bill_pkg->setup != 0;
3401 $late_subtotal{$section} += $cust_bill_pkg->recur
3402 if $cust_bill_pkg->recur != 0;
3405 if ($type && $type eq 'R') {
3406 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3407 if $cust_bill_pkg->recur != 0;
3410 if ($type && $type eq 'U') {
3411 $late_subtotal{$section} += $usage
3412 unless scalar(@$extra_sections);
3417 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3419 if (! $type || $type eq 'S') {
3420 $subtotal{$section} += $cust_bill_pkg->setup
3421 if $cust_bill_pkg->setup != 0;
3425 $subtotal{$section} += $cust_bill_pkg->recur
3426 if $cust_bill_pkg->recur != 0;
3429 if ($type && $type eq 'R') {
3430 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3431 if $cust_bill_pkg->recur != 0;
3434 if ($type && $type eq 'U') {
3435 $subtotal{$section} += $usage
3436 unless scalar(@$extra_sections);
3445 %pkg_category_cache = ();
3447 push @$late, map { { 'description' => &{$escape}($_),
3448 'subtotal' => $late_subtotal{$_},
3450 'sort_weight' => ( _pkg_category($_)
3451 ? _pkg_category($_)->weight
3454 ((_pkg_category($_) && _pkg_category($_)->condense)
3455 ? $self->_condense_section($format)
3459 sort _sectionsort keys %late_subtotal;
3462 if ( $summarypage ) {
3463 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3464 map { $_->categoryname } qsearch('pkg_category', {});
3465 push @sections, '' if exists($subtotal{''});
3467 @sections = keys %subtotal;
3470 my @early = map { { 'description' => &{$escape}($_),
3471 'subtotal' => $subtotal{$_},
3472 'summarized' => $not_tax{$_} ? '' : 'Y',
3473 'tax_section' => $not_tax{$_} ? '' : 'Y',
3474 'sort_weight' => ( _pkg_category($_)
3475 ? _pkg_category($_)->weight
3478 ((_pkg_category($_) && _pkg_category($_)->condense)
3479 ? $self->_condense_section($format)
3484 push @early, @$extra_sections if $extra_sections;
3486 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3490 #helper subs for above
3493 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3497 my $categoryname = shift;
3498 $pkg_category_cache{$categoryname} ||=
3499 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3502 my %condensed_format = (
3503 'label' => [ qw( Description Qty Amount ) ],
3505 sub { shift->{description} },
3506 sub { shift->{quantity} },
3507 sub { my($href, %opt) = @_;
3508 ($opt{dollar} || ''). $href->{amount};
3511 'align' => [ qw( l r r ) ],
3512 'span' => [ qw( 5 1 1 ) ], # unitprices?
3513 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3516 sub _condense_section {
3517 my ( $self, $format ) = ( shift, shift );
3519 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3520 qw( description_generator
3523 total_line_generator
3528 sub _condensed_generator_defaults {
3529 my ( $self, $format ) = ( shift, shift );
3530 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3539 sub _condensed_header_generator {
3540 my ( $self, $format ) = ( shift, shift );
3542 my ( $f, $prefix, $suffix, $separator, $column ) =
3543 _condensed_generator_defaults($format);
3545 if ($format eq 'latex') {
3546 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3547 $suffix = "\\\\\n\\hline";
3550 sub { my ($d,$a,$s,$w) = @_;
3551 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3553 } elsif ( $format eq 'html' ) {
3554 $prefix = '<th></th>';
3558 sub { my ($d,$a,$s,$w) = @_;
3559 return qq!<th align="$html_align{$a}">$d</th>!;
3567 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3569 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3572 $prefix. join($separator, @result). $suffix;
3577 sub _condensed_description_generator {
3578 my ( $self, $format ) = ( shift, shift );
3580 my ( $f, $prefix, $suffix, $separator, $column ) =
3581 _condensed_generator_defaults($format);
3583 my $money_char = '$';
3584 if ($format eq 'latex') {
3585 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3587 $separator = " & \n";
3589 sub { my ($d,$a,$s,$w) = @_;
3590 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3592 $money_char = '\\dollar';
3593 }elsif ( $format eq 'html' ) {
3594 $prefix = '"><td align="center"></td>';
3598 sub { my ($d,$a,$s,$w) = @_;
3599 return qq!<td align="$html_align{$a}">$d</td>!;
3601 #$money_char = $conf->config('money_char') || '$';
3602 $money_char = ''; # this is madness
3610 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3612 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3614 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3615 map { $f->{$_}->[$i] } qw(align span width)
3619 $prefix. join( $separator, @result ). $suffix;
3624 sub _condensed_total_generator {
3625 my ( $self, $format ) = ( shift, shift );
3627 my ( $f, $prefix, $suffix, $separator, $column ) =
3628 _condensed_generator_defaults($format);
3631 if ($format eq 'latex') {
3634 $separator = " & \n";
3636 sub { my ($d,$a,$s,$w) = @_;
3637 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3639 }elsif ( $format eq 'html' ) {
3643 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3645 sub { my ($d,$a,$s,$w) = @_;
3646 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3655 # my $r = &{$f->{fields}->[$i]}(@args);
3656 # $r .= ' Total' unless $i;
3658 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3660 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3661 map { $f->{$_}->[$i] } qw(align span width)
3665 $prefix. join( $separator, @result ). $suffix;
3670 =item total_line_generator FORMAT
3672 Returns a coderef used for generation of invoice total line items for this
3673 usage_class. FORMAT is either html or latex
3677 # should not be used: will have issues with hash element names (description vs
3678 # total_item and amount vs total_amount -- another array of functions?
3680 sub _condensed_total_line_generator {
3681 my ( $self, $format ) = ( shift, shift );
3683 my ( $f, $prefix, $suffix, $separator, $column ) =
3684 _condensed_generator_defaults($format);
3687 if ($format eq 'latex') {
3690 $separator = " & \n";
3692 sub { my ($d,$a,$s,$w) = @_;
3693 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3695 }elsif ( $format eq 'html' ) {
3699 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3701 sub { my ($d,$a,$s,$w) = @_;
3702 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3711 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3713 &{$column}( &{$f->{fields}->[$i]}(@args),
3714 map { $f->{$_}->[$i] } qw(align span width)
3718 $prefix. join( $separator, @result ). $suffix;
3723 #sub _items_extra_usage_sections {
3725 # my $escape = shift;
3727 # my %sections = ();
3729 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3730 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3732 # next unless $cust_bill_pkg->pkgnum > 0;
3734 # foreach my $section ( keys %usage_class ) {
3736 # my $usage = $cust_bill_pkg->usage($section);
3738 # next unless $usage && $usage > 0;
3740 # $sections{$section} ||= 0;
3741 # $sections{$section} += $usage;
3747 # map { { 'description' => &{$escape}($_),
3748 # 'subtotal' => $sections{$_},
3749 # 'summarized' => '',
3750 # 'tax_section' => '',
3753 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3757 sub _items_extra_usage_sections {
3766 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3767 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3768 next unless $cust_bill_pkg->pkgnum > 0;
3770 foreach my $classnum ( keys %usage_class ) {
3771 my $section = $usage_class{$classnum}->classname;
3772 $classnums{$section} = $classnum;
3774 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3775 my $amount = $detail->amount;
3776 next unless $amount && $amount > 0;
3778 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3779 $sections{$section}{amount} += $amount; #subtotal
3780 $sections{$section}{calls}++;
3781 $sections{$section}{duration} += $detail->duration;
3783 my $desc = $detail->regionname;
3784 my $description = $desc;
3785 $description = substr($desc, 0, 50). '...'
3786 if $format eq 'latex' && length($desc) > 50;
3788 $lines{$section}{$desc} ||= {
3789 description => &{$escape}($description),
3790 #pkgpart => $part_pkg->pkgpart,
3791 pkgnum => $cust_bill_pkg->pkgnum,
3796 #unit_amount => $cust_bill_pkg->unitrecur,
3797 quantity => $cust_bill_pkg->quantity,
3798 product_code => 'N/A',
3799 ext_description => [],
3802 $lines{$section}{$desc}{amount} += $amount;
3803 $lines{$section}{$desc}{calls}++;
3804 $lines{$section}{$desc}{duration} += $detail->duration;
3810 my %sectionmap = ();
3811 foreach (keys %sections) {
3812 my $usage_class = $usage_class{$classnums{$_}};
3813 $sectionmap{$_} = { 'description' => &{$escape}($_),
3814 'amount' => $sections{$_}{amount}, #subtotal
3815 'calls' => $sections{$_}{calls},
3816 'duration' => $sections{$_}{duration},
3818 'tax_section' => '',
3819 'sort_weight' => $usage_class->weight,
3820 ( $usage_class->format
3821 ? ( map { $_ => $usage_class->$_($format) }
3822 qw( description_generator header_generator total_generator total_line_generator )
3829 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3833 foreach my $section ( keys %lines ) {
3834 foreach my $line ( keys %{$lines{$section}} ) {
3835 my $l = $lines{$section}{$line};
3836 $l->{section} = $sectionmap{$section};
3837 $l->{amount} = sprintf( "%.2f", $l->{amount} );
3838 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3843 return(\@sections, \@lines);
3849 my $end = $self->_date;
3850 my $start = $end - 2592000; # 30 days
3851 my $cust_main = $self->cust_main;
3852 my @pkgs = $cust_main->all_pkgs;
3853 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3856 foreach my $pkg ( @pkgs ) {
3857 my @h_cust_svc = $pkg->h_cust_svc($end);
3858 foreach my $h_cust_svc ( @h_cust_svc ) {
3859 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3860 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3862 my $inserted = $h_cust_svc->date_inserted;
3863 my $deleted = $h_cust_svc->date_deleted;
3864 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3866 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
3868 # DID either activated or ported in; cannot be both for same DID simultaneously
3869 if ($inserted >= $start && $inserted <= $end && $phone_inserted
3870 && (!$phone_inserted->lnp_status
3871 || $phone_inserted->lnp_status eq ''
3872 || $phone_inserted->lnp_status eq 'native')) {
3875 else { # this one not so clean, should probably move to (h_)svc_phone
3876 my $phone_portedin = qsearchs( 'h_svc_phone',
3877 { 'svcnum' => $h_cust_svc->svcnum,
3878 'lnp_status' => 'portedin' },
3879 FS::h_svc_phone->sql_h_searchs($end),
3881 $num_portedin++ if $phone_portedin;
3884 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3885 if($deleted >= $start && $deleted <= $end && $phone_deleted
3886 && (!$phone_deleted->lnp_status
3887 || $phone_deleted->lnp_status ne 'portingout')) {
3890 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
3891 && $phone_deleted->lnp_status
3892 && $phone_deleted->lnp_status eq 'portingout') {
3896 # increment usage minutes
3897 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3898 foreach my $cdr ( @cdrs ) {
3899 $minutes += $cdr->billsec/60;
3902 # don't look at this service again
3903 push @seen, $h_cust_svc->svcnum;
3907 $minutes = sprintf("%d", $minutes);
3908 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
3909 . "$num_deactivated Ported-Out: $num_portedout ",
3910 "Total Minutes: $minutes");
3913 sub _items_svc_phone_sections {
3922 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3923 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3925 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3926 next unless $cust_bill_pkg->pkgnum > 0;
3928 my @header = $cust_bill_pkg->details_header;
3929 next unless scalar(@header);
3931 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3933 my $phonenum = $detail->phonenum;
3934 next unless $phonenum;
3936 my $amount = $detail->amount;
3937 next unless $amount && $amount > 0;
3939 $sections{$phonenum} ||= { 'amount' => 0,
3942 'sort_weight' => -1,
3943 'phonenum' => $phonenum,
3945 $sections{$phonenum}{amount} += $amount; #subtotal
3946 $sections{$phonenum}{calls}++;
3947 $sections{$phonenum}{duration} += $detail->duration;
3949 my $desc = $detail->regionname;
3950 my $description = $desc;
3951 $description = substr($desc, 0, 50). '...'
3952 if $format eq 'latex' && length($desc) > 50;
3954 $lines{$phonenum}{$desc} ||= {
3955 description => &{$escape}($description),
3956 #pkgpart => $part_pkg->pkgpart,
3964 product_code => 'N/A',
3965 ext_description => [],
3968 $lines{$phonenum}{$desc}{amount} += $amount;
3969 $lines{$phonenum}{$desc}{calls}++;
3970 $lines{$phonenum}{$desc}{duration} += $detail->duration;
3972 my $line = $usage_class{$detail->classnum}->classname;
3973 $sections{"$phonenum $line"} ||=
3977 'sort_weight' => $usage_class{$detail->classnum}->weight,
3978 'phonenum' => $phonenum,
3979 'header' => [ @header ],
3981 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
3982 $sections{"$phonenum $line"}{calls}++;
3983 $sections{"$phonenum $line"}{duration} += $detail->duration;
3985 $lines{"$phonenum $line"}{$desc} ||= {
3986 description => &{$escape}($description),
3987 #pkgpart => $part_pkg->pkgpart,
3995 product_code => 'N/A',
3996 ext_description => [],
3999 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4000 $lines{"$phonenum $line"}{$desc}{calls}++;
4001 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4002 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4003 $detail->formatted('format' => $format);
4008 my %sectionmap = ();
4009 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4010 foreach ( keys %sections ) {
4011 my @header = @{ $sections{$_}{header} || [] };
4013 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4014 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4015 my $usage_class = $summary ? $simple : $usage_simple;
4016 my $ending = $summary ? ' usage charges' : '';
4019 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4021 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4022 'amount' => $sections{$_}{amount}, #subtotal
4023 'calls' => $sections{$_}{calls},
4024 'duration' => $sections{$_}{duration},
4026 'tax_section' => '',
4027 'phonenum' => $sections{$_}{phonenum},
4028 'sort_weight' => $sections{$_}{sort_weight},
4029 'post_total' => $summary, #inspire pagebreak
4031 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4032 qw( description_generator
4035 total_line_generator
4042 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4043 $a->{sort_weight} <=> $b->{sort_weight}
4048 foreach my $section ( keys %lines ) {
4049 foreach my $line ( keys %{$lines{$section}} ) {
4050 my $l = $lines{$section}{$line};
4051 $l->{section} = $sectionmap{$section};
4052 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4053 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4058 return(\@sections, \@lines);
4065 #my @display = scalar(@_)
4067 # : qw( _items_previous _items_pkg );
4068 # #: qw( _items_pkg );
4069 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4070 my @display = qw( _items_previous _items_pkg );
4073 foreach my $display ( @display ) {
4074 push @b, $self->$display(@_);
4079 sub _items_previous {
4081 my $cust_main = $self->cust_main;
4082 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4084 foreach ( @pr_cust_bill ) {
4085 my $date = $conf->exists('invoice_show_prior_due_date')
4086 ? 'due '. $_->due_date2str($date_format)
4087 : time2str($date_format, $_->_date);
4089 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4090 #'pkgpart' => 'N/A',
4092 'amount' => sprintf("%.2f", $_->owed),
4098 # 'description' => 'Previous Balance',
4099 # #'pkgpart' => 'N/A',
4100 # 'pkgnum' => 'N/A',
4101 # 'amount' => sprintf("%10.2f", $pr_total ),
4102 # 'ext_description' => [ map {
4103 # "Invoice ". $_->invnum.
4104 # " (". time2str("%x",$_->_date). ") ".
4105 # sprintf("%10.2f", $_->owed)
4106 # } @pr_cust_bill ],
4114 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4115 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4116 if ($options{section} && $options{section}->{condensed}) {
4118 local $Storable::canonical = 1;
4119 foreach ( @items ) {
4121 delete $item->{ref};
4122 delete $item->{ext_description};
4123 my $key = freeze($item);
4124 $itemshash{$key} ||= 0;
4125 $itemshash{$key} ++; # += $item->{quantity};
4127 @items = sort { $a->{description} cmp $b->{description} }
4128 map { my $i = thaw($_);
4129 $i->{quantity} = $itemshash{$_};
4131 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4140 return 0 unless $a->itemdesc cmp $b->itemdesc;
4141 return -1 if $b->itemdesc eq 'Tax';
4142 return 1 if $a->itemdesc eq 'Tax';
4143 return -1 if $b->itemdesc eq 'Other surcharges';
4144 return 1 if $a->itemdesc eq 'Other surcharges';
4145 $a->itemdesc cmp $b->itemdesc;
4150 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4151 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4154 sub _items_cust_bill_pkg {
4156 my $cust_bill_pkg = shift;
4159 my $format = $opt{format} || '';
4160 my $escape_function = $opt{escape_function} || sub { shift };
4161 my $format_function = $opt{format_function} || '';
4162 my $unsquelched = $opt{unsquelched} || '';
4163 my $section = $opt{section}->{description} if $opt{section};
4164 my $summary_page = $opt{summary_page} || '';
4165 my $multilocation = $opt{multilocation} || '';
4166 my $multisection = $opt{multisection} || '';
4167 my $discount_show_always = 0;
4170 my ($s, $r, $u) = ( undef, undef, undef );
4171 foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4174 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4175 && $conf->exists('discount-show-always'));
4177 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4178 if ( $_ && !$cust_bill_pkg->hidden ) {
4179 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4180 $_->{amount} =~ s/^\-0\.00$/0.00/;
4181 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4183 unless ( $_->{amount} == 0 && !$discount_show_always );
4188 foreach my $display ( grep { defined($section)
4189 ? $_->section eq $section
4192 #grep { !$_->summary || !$summary_page } # bunk!
4193 grep { !$_->summary || $multisection }
4194 $cust_bill_pkg->cust_bill_pkg_display
4198 my $type = $display->type;
4200 my $desc = $cust_bill_pkg->desc;
4201 $desc = substr($desc, 0, 50). '...'
4202 if $format eq 'latex' && length($desc) > 50;
4204 my %details_opt = ( 'format' => $format,
4205 'escape_function' => $escape_function,
4206 'format_function' => $format_function,
4209 if ( $cust_bill_pkg->pkgnum > 0 ) {
4211 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4213 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4215 my $description = $desc;
4216 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4219 unless ( $cust_pkg->part_pkg->hide_svc_detail
4220 || $cust_bill_pkg->hidden )
4223 push @d, map &{$escape_function}($_),
4224 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4225 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4227 if ( $multilocation ) {
4228 my $loc = $cust_pkg->location_label;
4229 $loc = substr($loc, 0, 50). '...'
4230 if $format eq 'latex' && length($loc) > 50;
4231 push @d, &{$escape_function}($loc);
4236 push @d, $cust_bill_pkg->details(%details_opt)
4237 if $cust_bill_pkg->recur == 0;
4239 if ( $cust_bill_pkg->hidden ) {
4240 $s->{amount} += $cust_bill_pkg->setup;
4241 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4242 push @{ $s->{ext_description} }, @d;
4245 description => $description,
4246 #pkgpart => $part_pkg->pkgpart,
4247 pkgnum => $cust_bill_pkg->pkgnum,
4248 amount => $cust_bill_pkg->setup,
4249 unit_amount => $cust_bill_pkg->unitsetup,
4250 quantity => $cust_bill_pkg->quantity,
4251 ext_description => \@d,
4257 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4258 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4259 ( !$type || $type eq 'R' || $type eq 'U' )
4263 my $is_summary = $display->summary;
4264 my $description = ($is_summary && $type && $type eq 'U')
4265 ? "Usage charges" : $desc;
4267 unless ( $conf->exists('disable_line_item_date_ranges') ) {
4268 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4269 " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4274 #at least until cust_bill_pkg has "past" ranges in addition to
4275 #the "future" sdate/edate ones... see #3032
4276 my @dates = ( $self->_date );
4277 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4278 push @dates, $prev->sdate if $prev;
4279 push @dates, undef if !$prev;
4281 unless ( $cust_pkg->part_pkg->hide_svc_detail
4282 || $cust_bill_pkg->itemdesc
4283 || $cust_bill_pkg->hidden
4284 || $is_summary && $type && $type eq 'U' )
4287 push @d, map &{$escape_function}($_),
4288 $cust_pkg->h_labels_short(@dates, 'I')
4289 #$cust_bill_pkg->edate,
4290 #$cust_bill_pkg->sdate)
4291 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4293 if ( $multilocation ) {
4294 my $loc = $cust_pkg->location_label;
4295 $loc = substr($loc, 0, 50). '...'
4296 if $format eq 'latex' && length($loc) > 50;
4297 push @d, &{$escape_function}($loc);
4302 push @d, $cust_bill_pkg->details(%details_opt)
4303 unless ($is_summary || $type && $type eq 'R');
4307 $amount = $cust_bill_pkg->recur;
4308 }elsif($type eq 'R') {
4309 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4310 }elsif($type eq 'U') {
4311 $amount = $cust_bill_pkg->usage;
4314 if ( !$type || $type eq 'R' ) {
4316 if ( $cust_bill_pkg->hidden ) {
4317 $r->{amount} += $amount;
4318 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4319 push @{ $r->{ext_description} }, @d;
4322 description => $description,
4323 #pkgpart => $part_pkg->pkgpart,
4324 pkgnum => $cust_bill_pkg->pkgnum,
4326 unit_amount => $cust_bill_pkg->unitrecur,
4327 quantity => $cust_bill_pkg->quantity,
4328 ext_description => \@d,
4332 } else { # $type eq 'U'
4334 if ( $cust_bill_pkg->hidden ) {
4335 $u->{amount} += $amount;
4336 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4337 push @{ $u->{ext_description} }, @d;
4340 description => $description,
4341 #pkgpart => $part_pkg->pkgpart,
4342 pkgnum => $cust_bill_pkg->pkgnum,
4344 unit_amount => $cust_bill_pkg->unitrecur,
4345 quantity => $cust_bill_pkg->quantity,
4346 ext_description => \@d,
4352 } # recurring or usage with recurring charge
4354 } else { #pkgnum tax or one-shot line item (??)
4356 if ( $cust_bill_pkg->setup != 0 ) {
4358 'description' => $desc,
4359 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4362 if ( $cust_bill_pkg->recur != 0 ) {
4364 'description' => "$desc (".
4365 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4366 time2str($date_format, $cust_bill_pkg->edate). ')',
4367 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4377 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4379 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4380 $_->{amount} =~ s/^\-0\.00$/0.00/;
4381 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4383 unless ( $_->{amount} == 0 && !$discount_show_always );
4391 sub _items_credits {
4392 my( $self, %opt ) = @_;
4393 my $trim_len = $opt{'trim_len'} || 60;
4397 foreach ( $self->cust_credited ) {
4399 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4401 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4402 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4403 $reason = " ($reason) " if $reason;
4406 #'description' => 'Credit ref\#'. $_->crednum.
4407 # " (". time2str("%x",$_->cust_credit->_date) .")".
4409 'description' => 'Credit applied '.
4410 time2str($date_format,$_->cust_credit->_date). $reason,
4411 'amount' => sprintf("%.2f",$_->amount),
4419 sub _items_payments {
4423 #get & print payments
4424 foreach ( $self->cust_bill_pay ) {
4426 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4429 'description' => "Payment received ".
4430 time2str($date_format,$_->cust_pay->_date ),
4431 'amount' => sprintf("%.2f", $_->amount )
4439 =item call_details [ OPTION => VALUE ... ]
4441 Returns an array of CSV strings representing the call details for this invoice
4442 The only option available is the boolean prepend_billed_number
4447 my ($self, %opt) = @_;
4449 my $format_function = sub { shift };
4451 if ($opt{prepend_billed_number}) {
4452 $format_function = sub {
4456 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4461 my @details = map { $_->details( 'format_function' => $format_function,
4462 'escape_function' => sub{ return() },
4466 $self->cust_bill_pkg;
4467 my $header = $details[0];
4468 ( $header, grep { $_ ne $header } @details );
4478 =item process_reprint
4482 sub process_reprint {
4483 process_re_X('print', @_);
4486 =item process_reemail
4490 sub process_reemail {
4491 process_re_X('email', @_);
4499 process_re_X('fax', @_);
4507 process_re_X('ftp', @_);
4514 sub process_respool {
4515 process_re_X('spool', @_);
4518 use Storable qw(thaw);
4522 my( $method, $job ) = ( shift, shift );
4523 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4525 my $param = thaw(decode_base64(shift));
4526 warn Dumper($param) if $DEBUG;
4537 my($method, $job, %param ) = @_;
4539 warn "re_X $method for job $job with param:\n".
4540 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4543 #some false laziness w/search/cust_bill.html
4545 my $orderby = 'ORDER BY cust_bill._date';
4547 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4549 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4551 my @cust_bill = qsearch( {
4552 #'select' => "cust_bill.*",
4553 'table' => 'cust_bill',
4554 'addl_from' => $addl_from,
4556 'extra_sql' => $extra_sql,
4557 'order_by' => $orderby,
4561 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4563 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4566 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4567 foreach my $cust_bill ( @cust_bill ) {
4568 $cust_bill->$method();
4570 if ( $job ) { #progressbar foo
4572 if ( time - $min_sec > $last ) {
4573 my $error = $job->update_statustext(
4574 int( 100 * $num / scalar(@cust_bill) )
4576 die $error if $error;
4587 =head1 CLASS METHODS
4593 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4598 my ($class, $start, $end) = @_;
4600 $class->paid_sql($start, $end). ' - '.
4601 $class->credited_sql($start, $end);
4606 Returns an SQL fragment to retreive the net amount (charged minus credited).
4611 my ($class, $start, $end) = @_;
4612 'charged - '. $class->credited_sql($start, $end);
4617 Returns an SQL fragment to retreive the amount paid against this invoice.
4622 my ($class, $start, $end) = @_;
4623 $start &&= "AND cust_bill_pay._date <= $start";
4624 $end &&= "AND cust_bill_pay._date > $end";
4625 $start = '' unless defined($start);
4626 $end = '' unless defined($end);
4627 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4628 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4633 Returns an SQL fragment to retreive the amount credited against this invoice.
4638 my ($class, $start, $end) = @_;
4639 $start &&= "AND cust_credit_bill._date <= $start";
4640 $end &&= "AND cust_credit_bill._date > $end";
4641 $start = '' unless defined($start);
4642 $end = '' unless defined($end);
4643 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4644 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4649 Returns an SQL fragment to retrieve the due date of an invoice.
4650 Currently only supported on PostgreSQL.
4658 cust_bill.invoice_terms,
4659 cust_main.invoice_terms,
4660 \''.($conf->config('invoice_default_terms') || '').'\'
4661 ), E\'Net (\\\\d+)\'
4663 ) * 86400 + cust_bill._date'
4666 =item search_sql_where HASHREF
4668 Class method which returns an SQL WHERE fragment to search for parameters
4669 specified in HASHREF. Valid parameters are
4675 List reference of start date, end date, as UNIX timestamps.
4685 List reference of charged limits (exclusive).
4689 List reference of charged limits (exclusive).
4693 flag, return open invoices only
4697 flag, return net invoices only
4701 =item newest_percust
4705 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4709 sub search_sql_where {
4710 my($class, $param) = @_;
4712 warn "$me search_sql_where called with params: \n".
4713 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
4719 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4720 push @search, "cust_main.agentnum = $1";
4724 if ( $param->{_date} ) {
4725 my($beginning, $ending) = @{$param->{_date}};
4727 push @search, "cust_bill._date >= $beginning",
4728 "cust_bill._date < $ending";
4732 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4733 push @search, "cust_bill.invnum >= $1";
4735 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4736 push @search, "cust_bill.invnum <= $1";
4740 if ( $param->{charged} ) {
4741 my @charged = ref($param->{charged})
4742 ? @{ $param->{charged} }
4743 : ($param->{charged});
4745 push @search, map { s/^charged/cust_bill.charged/; $_; }
4749 my $owed_sql = FS::cust_bill->owed_sql;
4752 if ( $param->{owed} ) {
4753 my @owed = ref($param->{owed})
4754 ? @{ $param->{owed} }
4756 push @search, map { s/^owed/$owed_sql/; $_; }
4761 push @search, "0 != $owed_sql"
4762 if $param->{'open'};
4763 push @search, '0 != '. FS::cust_bill->net_sql
4767 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4768 if $param->{'days'};
4771 if ( $param->{'newest_percust'} ) {
4773 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4774 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4776 my @newest_where = map { my $x = $_;
4777 $x =~ s/\bcust_bill\./newest_cust_bill./g;
4780 grep ! /^cust_main./, @search;
4781 my $newest_where = scalar(@newest_where)
4782 ? ' AND '. join(' AND ', @newest_where)
4786 push @search, "cust_bill._date = (
4787 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4788 WHERE newest_cust_bill.custnum = cust_bill.custnum
4794 #agent virtualization
4795 my $curuser = $FS::CurrentUser::CurrentUser;
4796 if ( $curuser->username eq 'fs_queue'
4797 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4799 my $newuser = qsearchs('access_user', {
4800 'username' => $username,
4804 $curuser = $newuser;
4806 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4809 push @search, $curuser->agentnums_sql;
4811 join(' AND ', @search );
4823 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4824 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base