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
9 use List::Util qw(min max);
11 use Text::Template 1.20;
13 use String::ShellQuote;
16 use Storable qw( freeze thaw );
18 use FS::UID qw( datasrc );
19 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
20 use FS::Record qw( qsearch qsearchs dbh );
21 use FS::cust_main_Mixin;
23 use FS::cust_statement;
24 use FS::cust_bill_pkg;
25 use FS::cust_bill_pkg_display;
26 use FS::cust_bill_pkg_detail;
30 use FS::cust_credit_bill;
32 use FS::cust_pay_batch;
33 use FS::cust_bill_event;
36 use FS::cust_bill_pay;
37 use FS::cust_bill_pay_batch;
38 use FS::part_bill_event;
41 use FS::cust_bill_batch;
42 use FS::cust_bill_pay_pkg;
43 use FS::cust_credit_bill_pkg;
45 @ISA = qw( FS::cust_main_Mixin FS::Record );
48 $me = '[FS::cust_bill]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $money_char = $conf->config('money_char') || '$';
54 $date_format = $conf->config('date_format') || '%x'; #/YY
55 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
56 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 FS::cust_bill - Object methods for cust_bill records
67 $record = new FS::cust_bill \%hash;
68 $record = new FS::cust_bill { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
80 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
82 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
84 @cust_pay_objects = $cust_bill->cust_pay;
86 $tax_amount = $record->tax;
88 @lines = $cust_bill->print_text;
89 @lines = $cust_bill->print_text $time;
93 An FS::cust_bill object represents an invoice; a declaration that a customer
94 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
95 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
96 following fields are currently supported:
102 =item invnum - primary key (assigned automatically for new invoices)
104 =item custnum - customer (see L<FS::cust_main>)
106 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
107 L<Time::Local> and L<Date::Parse> for conversion functions.
109 =item charged - amount of this invoice
111 =item invoice_terms - optional terms override for this specific invoice
115 Customer info at invoice generation time
119 =item previous_balance
121 =item billing_balance
129 =item printed - deprecated
137 =item closed - books closed flag, empty or `Y'
139 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
141 =item agent_invid - legacy invoice number
151 Creates a new invoice. To add the invoice to the database, see L<"insert">.
152 Invoices are normally created by calling the bill method of a customer object
153 (see L<FS::cust_main>).
157 sub table { 'cust_bill'; }
159 sub cust_linked { $_[0]->cust_main_custnum; }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 This method now works but you probably shouldn't use it. Instead, apply a
213 credit against the invoice.
215 Using this method to delete invoices outright is really, really bad. There
216 would be no record you ever posted this invoice, and there are no check to
217 make sure charged = 0 or that there are no associated cust_bill_pkg records.
219 Really, don't use it.
225 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 foreach my $table (qw(
250 foreach my $linked ( $self->$table() ) {
251 my $error = $linked->delete;
253 $dbh->rollback if $oldAutoCommit;
260 my $error = $self->SUPER::delete(@_);
262 $dbh->rollback if $oldAutoCommit;
266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
272 =item replace [ OLD_RECORD ]
274 You can, but probably shouldn't modify invoices...
276 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
277 supplied, replaces this record. If there is an error, returns the error,
278 otherwise returns false.
282 #replace can be inherited from Record.pm
284 # replace_check is now the preferred way to #implement replace data checks
285 # (so $object->replace() works without an argument)
288 my( $new, $old ) = ( shift, shift );
289 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
290 #return "Can't change _date!" unless $old->_date eq $new->_date;
291 return "Can't change _date" unless $old->_date == $new->_date;
292 return "Can't change charged" unless $old->charged == $new->charged
293 || $old->charged == 0;
300 Checks all fields to make sure this is a valid invoice. If there is an error,
301 returns the error, otherwise returns false. Called by the insert and replace
310 $self->ut_numbern('invnum')
311 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
312 || $self->ut_numbern('_date')
313 || $self->ut_money('charged')
314 || $self->ut_numbern('printed')
315 || $self->ut_enum('closed', [ '', 'Y' ])
316 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
317 || $self->ut_numbern('agent_invid') #varchar?
319 return $error if $error;
321 $self->_date(time) unless $self->_date;
323 $self->printed(0) if $self->printed eq '';
330 Returns the displayed invoice number for this invoice: agent_invid if
331 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
337 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
338 return $self->agent_invid;
340 return $self->invnum;
346 Returns a list consisting of the total previous balance for this customer,
347 followed by the previous outstanding invoices (as FS::cust_bill objects also).
354 my @cust_bill = sort { $a->_date <=> $b->_date }
355 grep { $_->owed != 0 && $_->_date < $self->_date }
356 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
358 foreach ( @cust_bill ) { $total += $_->owed; }
364 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
371 { 'table' => 'cust_bill_pkg',
372 'hashref' => { 'invnum' => $self->invnum },
373 'order_by' => 'ORDER BY billpkgnum',
378 =item cust_bill_pkg_pkgnum PKGNUM
380 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
385 sub cust_bill_pkg_pkgnum {
386 my( $self, $pkgnum ) = @_;
388 { 'table' => 'cust_bill_pkg',
389 'hashref' => { 'invnum' => $self->invnum,
392 'order_by' => 'ORDER BY billpkgnum',
399 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
406 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
407 $self->cust_bill_pkg;
409 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
414 Returns true if any of the packages (or their definitions) corresponding to the
415 line items for this invoice have the no_auto flag set.
421 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
424 =item open_cust_bill_pkg
426 Returns the open line items for this invoice.
428 Note that cust_bill_pkg with both setup and recur fees are returned as two
429 separate line items, each with only one fee.
433 # modeled after cust_main::open_cust_bill
434 sub open_cust_bill_pkg {
437 # grep { $_->owed > 0 } $self->cust_bill_pkg
439 my %other = ( 'recur' => 'setup',
440 'setup' => 'recur', );
442 foreach my $field ( qw( recur setup )) {
443 push @open, map { $_->set( $other{$field}, 0 ); $_; }
444 grep { $_->owed($field) > 0 }
445 $self->cust_bill_pkg;
451 =item cust_bill_event
453 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
457 sub cust_bill_event {
459 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
462 =item num_cust_bill_event
464 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
468 sub num_cust_bill_event {
471 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
472 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
473 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
474 $sth->fetchrow_arrayref->[0];
479 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
483 #false laziness w/cust_pkg.pm
487 'table' => 'cust_event',
488 'addl_from' => 'JOIN part_event USING ( eventpart )',
489 'hashref' => { 'tablenum' => $self->invnum },
490 'extra_sql' => " AND eventtable = 'cust_bill' ",
496 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
500 #false laziness w/cust_pkg.pm
504 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
505 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
506 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
507 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
508 $sth->fetchrow_arrayref->[0];
513 Returns the customer (see L<FS::cust_main>) for this invoice.
519 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
522 =item cust_suspend_if_balance_over AMOUNT
524 Suspends the customer associated with this invoice if the total amount owed on
525 this invoice and all older invoices is greater than the specified amount.
527 Returns a list: an empty list on success or a list of errors.
531 sub cust_suspend_if_balance_over {
532 my( $self, $amount ) = ( shift, shift );
533 my $cust_main = $self->cust_main;
534 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
537 $cust_main->suspend(@_);
543 Depreciated. See the cust_credited method.
545 #Returns a list consisting of the total previous credited (see
546 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
547 #outstanding credits (FS::cust_credit objects).
553 croak "FS::cust_bill->cust_credit depreciated; see ".
554 "FS::cust_bill->cust_credit_bill";
557 #my @cust_credit = sort { $a->_date <=> $b->_date }
558 # grep { $_->credited != 0 && $_->_date < $self->_date }
559 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
561 #foreach (@cust_credit) { $total += $_->credited; }
562 #$total, @cust_credit;
567 Depreciated. See the cust_bill_pay method.
569 #Returns all payments (see L<FS::cust_pay>) for this invoice.
575 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
577 #sort { $a->_date <=> $b->_date }
578 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
584 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
587 sub cust_bill_pay_batch {
589 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
594 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
600 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
601 sort { $a->_date <=> $b->_date }
602 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
607 =item cust_credit_bill
609 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
615 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
616 sort { $a->_date <=> $b->_date }
617 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
621 sub cust_credit_bill {
622 shift->cust_credited(@_);
625 #=item cust_bill_pay_pkgnum PKGNUM
627 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
628 #with matching pkgnum.
632 #sub cust_bill_pay_pkgnum {
633 # my( $self, $pkgnum ) = @_;
634 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
635 # sort { $a->_date <=> $b->_date }
636 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
637 # 'pkgnum' => $pkgnum,
642 =item cust_bill_pay_pkg PKGNUM
644 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
645 applied against the matching pkgnum.
649 sub cust_bill_pay_pkg {
650 my( $self, $pkgnum ) = @_;
653 'select' => 'cust_bill_pay_pkg.*',
654 'table' => 'cust_bill_pay_pkg',
655 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
656 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
657 'hashref' => { 'invnum' => $self->invnum,
664 #=item cust_credited_pkgnum PKGNUM
666 #=item cust_credit_bill_pkgnum PKGNUM
668 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
669 #with matching pkgnum.
673 #sub cust_credited_pkgnum {
674 # my( $self, $pkgnum ) = @_;
675 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
676 # sort { $a->_date <=> $b->_date }
677 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
678 # 'pkgnum' => $pkgnum,
683 #sub cust_credit_bill_pkgnum {
684 # shift->cust_credited_pkgnum(@_);
687 =item cust_credit_bill_pkg PKGNUM
689 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
690 applied against the matching pkgnum.
694 sub cust_credit_bill_pkg {
695 my( $self, $pkgnum ) = @_;
698 'select' => 'cust_credit_bill_pkg.*',
699 'table' => 'cust_credit_bill_pkg',
700 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
701 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
702 'hashref' => { 'invnum' => $self->invnum,
711 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
718 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
720 foreach (@taxlines) { $total += $_->setup; }
726 Returns the amount owed (still outstanding) on this invoice, which is charged
727 minus all payment applications (see L<FS::cust_bill_pay>) and credit
728 applications (see L<FS::cust_credit_bill>).
734 my $balance = $self->charged;
735 $balance -= $_->amount foreach ( $self->cust_bill_pay );
736 $balance -= $_->amount foreach ( $self->cust_credited );
737 $balance = sprintf( "%.2f", $balance);
738 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
743 my( $self, $pkgnum ) = @_;
745 #my $balance = $self->charged;
747 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
749 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
750 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
752 $balance = sprintf( "%.2f", $balance);
753 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
757 =item apply_payments_and_credits [ OPTION => VALUE ... ]
759 Applies unapplied payments and credits to this invoice.
761 A hash of optional arguments may be passed. Currently "manual" is supported.
762 If true, a payment receipt is sent instead of a statement when
763 'payment_receipt_email' configuration option is set.
765 If there is an error, returns the error, otherwise returns false.
769 sub apply_payments_and_credits {
770 my( $self, %options ) = @_;
772 local $SIG{HUP} = 'IGNORE';
773 local $SIG{INT} = 'IGNORE';
774 local $SIG{QUIT} = 'IGNORE';
775 local $SIG{TERM} = 'IGNORE';
776 local $SIG{TSTP} = 'IGNORE';
777 local $SIG{PIPE} = 'IGNORE';
779 my $oldAutoCommit = $FS::UID::AutoCommit;
780 local $FS::UID::AutoCommit = 0;
783 $self->select_for_update; #mutex
785 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
786 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
788 if ( $conf->exists('pkg-balances') ) {
789 # limit @payments & @credits to those w/ a pkgnum grepped from $self
790 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
791 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
792 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
795 while ( $self->owed > 0 and ( @payments || @credits ) ) {
798 if ( @payments && @credits ) {
800 #decide which goes first by weight of top (unapplied) line item
802 my @open_lineitems = $self->open_cust_bill_pkg;
805 max( map { $_->part_pkg->pay_weight || 0 }
810 my $max_credit_weight =
811 max( map { $_->part_pkg->credit_weight || 0 }
817 #if both are the same... payments first? it has to be something
818 if ( $max_pay_weight >= $max_credit_weight ) {
824 } elsif ( @payments ) {
826 } elsif ( @credits ) {
829 die "guru meditation #12 and 35";
833 if ( $app eq 'pay' ) {
835 my $payment = shift @payments;
836 $unapp_amount = $payment->unapplied;
837 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
838 $app->pkgnum( $payment->pkgnum )
839 if $conf->exists('pkg-balances') && $payment->pkgnum;
841 } elsif ( $app eq 'credit' ) {
843 my $credit = shift @credits;
844 $unapp_amount = $credit->credited;
845 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
846 $app->pkgnum( $credit->pkgnum )
847 if $conf->exists('pkg-balances') && $credit->pkgnum;
850 die "guru meditation #12 and 35";
854 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
855 warn "owed_pkgnum ". $app->pkgnum;
856 $owed = $self->owed_pkgnum($app->pkgnum);
860 next unless $owed > 0;
862 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
863 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
865 $app->invnum( $self->invnum );
867 my $error = $app->insert(%options);
869 $dbh->rollback if $oldAutoCommit;
870 return "Error inserting ". $app->table. " record: $error";
872 die $error if $error;
876 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
881 =item generate_email OPTION => VALUE ...
889 sender address, required
893 alternate template name, optional
897 text attachment arrayref, optional
901 email subject, optional
905 notice name instead of "Invoice", optional
909 Returns an argument list to be passed to L<FS::Misc::send_email>.
920 my $me = '[FS::cust_bill::generate_email]';
923 'from' => $args{'from'},
924 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
928 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
929 'template' => $args{'template'},
930 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
933 my $cust_main = $self->cust_main;
935 if (ref($args{'to'}) eq 'ARRAY') {
936 $return{'to'} = $args{'to'};
938 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
939 $cust_main->invoicing_list
943 if ( $conf->exists('invoice_html') ) {
945 warn "$me creating HTML/text multipart message"
948 $return{'nobody'} = 1;
950 my $alternative = build MIME::Entity
951 'Type' => 'multipart/alternative',
952 'Encoding' => '7bit',
953 'Disposition' => 'inline'
957 if ( $conf->exists('invoice_email_pdf')
958 and scalar($conf->config('invoice_email_pdf_note')) ) {
960 warn "$me using 'invoice_email_pdf_note' in multipart message"
962 $data = [ map { $_ . "\n" }
963 $conf->config('invoice_email_pdf_note')
968 warn "$me not using 'invoice_email_pdf_note' in multipart message"
970 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
971 $data = $args{'print_text'};
973 $data = [ $self->print_text(\%opt) ];
978 $alternative->attach(
979 'Type' => 'text/plain',
980 #'Encoding' => 'quoted-printable',
981 'Encoding' => '7bit',
983 'Disposition' => 'inline',
986 $args{'from'} =~ /\@([\w\.\-]+)/;
987 my $from = $1 || 'example.com';
988 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
991 my $agentnum = $cust_main->agentnum;
992 if ( defined($args{'template'}) && length($args{'template'})
993 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
996 $logo = 'logo_'. $args{'template'}. '.png';
1000 my $image_data = $conf->config_binary( $logo, $agentnum);
1002 my $image = build MIME::Entity
1003 'Type' => 'image/png',
1004 'Encoding' => 'base64',
1005 'Data' => $image_data,
1006 'Filename' => 'logo.png',
1007 'Content-ID' => "<$content_id>",
1011 if($conf->exists('invoice-barcode')){
1012 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1013 $barcode = build MIME::Entity
1014 'Type' => 'image/png',
1015 'Encoding' => 'base64',
1016 'Data' => $self->invoice_barcode(0),
1017 'Filename' => 'barcode.png',
1018 'Content-ID' => "<$barcode_content_id>",
1020 $opt{'barcode_cid'} = $barcode_content_id;
1023 $alternative->attach(
1024 'Type' => 'text/html',
1025 'Encoding' => 'quoted-printable',
1026 'Data' => [ '<html>',
1029 ' '. encode_entities($return{'subject'}),
1032 ' <body bgcolor="#e8e8e8">',
1033 $self->print_html({ 'cid'=>$content_id, %opt }),
1037 'Disposition' => 'inline',
1038 #'Filename' => 'invoice.pdf',
1041 my @otherparts = ();
1042 if ( $cust_main->email_csv_cdr ) {
1044 push @otherparts, build MIME::Entity
1045 'Type' => 'text/csv',
1046 'Encoding' => '7bit',
1047 'Data' => [ map { "$_\n" }
1048 $self->call_details('prepend_billed_number' => 1)
1050 'Disposition' => 'attachment',
1051 'Filename' => 'usage-'. $self->invnum. '.csv',
1056 if ( $conf->exists('invoice_email_pdf') ) {
1061 # multipart/alternative
1067 my $related = build MIME::Entity 'Type' => 'multipart/related',
1068 'Encoding' => '7bit';
1070 #false laziness w/Misc::send_email
1071 $related->head->replace('Content-type',
1072 $related->mime_type.
1073 '; boundary="'. $related->head->multipart_boundary. '"'.
1074 '; type=multipart/alternative'
1077 $related->add_part($alternative);
1079 $related->add_part($image);
1081 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1083 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1087 #no other attachment:
1089 # multipart/alternative
1094 $return{'content-type'} = 'multipart/related';
1095 if($conf->exists('invoice-barcode')){
1096 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1099 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1101 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1102 #$return{'disposition'} = 'inline';
1108 if ( $conf->exists('invoice_email_pdf') ) {
1109 warn "$me creating PDF attachment"
1112 #mime parts arguments a la MIME::Entity->build().
1113 $return{'mimeparts'} = [
1114 { $self->mimebuild_pdf(\%opt) }
1118 if ( $conf->exists('invoice_email_pdf')
1119 and scalar($conf->config('invoice_email_pdf_note')) ) {
1121 warn "$me using 'invoice_email_pdf_note'"
1123 $return{'body'} = [ map { $_ . "\n" }
1124 $conf->config('invoice_email_pdf_note')
1129 warn "$me not using 'invoice_email_pdf_note'"
1131 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1132 $return{'body'} = $args{'print_text'};
1134 $return{'body'} = [ $self->print_text(\%opt) ];
1147 Returns a list suitable for passing to MIME::Entity->build(), representing
1148 this invoice as PDF attachment.
1155 'Type' => 'application/pdf',
1156 'Encoding' => 'base64',
1157 'Data' => [ $self->print_pdf(@_) ],
1158 'Disposition' => 'attachment',
1159 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1163 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1165 Sends this invoice to the destinations configured for this customer: sends
1166 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1168 Options can be passed as a hashref (recommended) or as a list of up to
1169 four values for templatename, agentnum, invoice_from and amount.
1171 I<template>, if specified, is the name of a suffix for alternate invoices.
1173 I<agentnum>, if specified, means that this invoice will only be sent for customers
1174 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1175 single agent) or an arrayref of agentnums.
1177 I<invoice_from>, if specified, overrides the default email invoice From: address.
1179 I<amount>, if specified, only sends the invoice if the total amount owed on this
1180 invoice and all older invoices is greater than the specified amount.
1182 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1186 sub queueable_send {
1189 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1190 or die "invalid invoice number: " . $opt{invnum};
1192 my @args = ( $opt{template}, $opt{agentnum} );
1193 push @args, $opt{invoice_from}
1194 if exists($opt{invoice_from}) && $opt{invoice_from};
1196 my $error = $self->send( @args );
1197 die $error if $error;
1204 my( $template, $invoice_from, $notice_name );
1206 my $balance_over = 0;
1210 $template = $opt->{'template'} || '';
1211 if ( $agentnums = $opt->{'agentnum'} ) {
1212 $agentnums = [ $agentnums ] unless ref($agentnums);
1214 $invoice_from = $opt->{'invoice_from'};
1215 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1216 $notice_name = $opt->{'notice_name'};
1218 $template = scalar(@_) ? shift : '';
1219 if ( scalar(@_) && $_[0] ) {
1220 $agentnums = ref($_[0]) ? shift : [ shift ];
1222 $invoice_from = shift if scalar(@_);
1223 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1226 return 'N/A' unless ! $agentnums
1227 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1230 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1232 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1233 $conf->config('invoice_from', $self->cust_main->agentnum );
1236 'template' => $template,
1237 'invoice_from' => $invoice_from,
1238 'notice_name' => ( $notice_name || 'Invoice' ),
1241 my @invoicing_list = $self->cust_main->invoicing_list;
1243 #$self->email_invoice(\%opt)
1245 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1247 #$self->print_invoice(\%opt)
1249 if grep { $_ eq 'POST' } @invoicing_list; #postal
1251 $self->fax_invoice(\%opt)
1252 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1258 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1260 Emails this invoice.
1262 Options can be passed as a hashref (recommended) or as a list of up to
1263 two values for templatename and invoice_from.
1265 I<template>, if specified, is the name of a suffix for alternate invoices.
1267 I<invoice_from>, if specified, overrides the default email invoice From: address.
1269 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1273 sub queueable_email {
1276 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1277 or die "invalid invoice number: " . $opt{invnum};
1279 my @args = ( $opt{template} );
1280 push @args, $opt{invoice_from}
1281 if exists($opt{invoice_from}) && $opt{invoice_from};
1283 my $error = $self->email( @args );
1284 die $error if $error;
1288 #sub email_invoice {
1292 my( $template, $invoice_from, $notice_name );
1295 $template = $opt->{'template'} || '';
1296 $invoice_from = $opt->{'invoice_from'};
1297 $notice_name = $opt->{'notice_name'} || 'Invoice';
1299 $template = scalar(@_) ? shift : '';
1300 $invoice_from = shift if scalar(@_);
1301 $notice_name = 'Invoice';
1304 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1305 $conf->config('invoice_from', $self->cust_main->agentnum );
1307 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1308 $self->cust_main->invoicing_list;
1310 if ( ! @invoicing_list ) { #no recipients
1311 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1312 die 'No recipients for customer #'. $self->custnum;
1314 #default: better to notify this person than silence
1315 @invoicing_list = ($invoice_from);
1319 my $subject = $self->email_subject($template);
1321 my $error = send_email(
1322 $self->generate_email(
1323 'from' => $invoice_from,
1324 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1325 'subject' => $subject,
1326 'template' => $template,
1327 'notice_name' => $notice_name,
1330 die "can't email invoice: $error\n" if $error;
1331 #die "$error\n" if $error;
1338 #my $template = scalar(@_) ? shift : '';
1341 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1344 my $cust_main = $self->cust_main;
1345 my $name = $cust_main->name;
1346 my $name_short = $cust_main->name_short;
1347 my $invoice_number = $self->invnum;
1348 my $invoice_date = $self->_date_pretty;
1350 eval qq("$subject");
1353 =item lpr_data HASHREF | [ TEMPLATE ]
1355 Returns the postscript or plaintext for this invoice as an arrayref.
1357 Options can be passed as a hashref (recommended) or as a single optional value
1360 I<template>, if specified, is the name of a suffix for alternate invoices.
1362 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1368 my( $template, $notice_name );
1371 $template = $opt->{'template'} || '';
1372 $notice_name = $opt->{'notice_name'} || 'Invoice';
1374 $template = scalar(@_) ? shift : '';
1375 $notice_name = 'Invoice';
1379 'template' => $template,
1380 'notice_name' => $notice_name,
1383 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1384 [ $self->$method( \%opt ) ];
1387 =item print HASHREF | [ TEMPLATE ]
1389 Prints this invoice.
1391 Options can be passed as a hashref (recommended) or as a single optional
1394 I<template>, if specified, is the name of a suffix for alternate invoices.
1396 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1400 #sub print_invoice {
1403 my( $template, $notice_name );
1406 $template = $opt->{'template'} || '';
1407 $notice_name = $opt->{'notice_name'} || 'Invoice';
1409 $template = scalar(@_) ? shift : '';
1410 $notice_name = 'Invoice';
1414 'template' => $template,
1415 'notice_name' => $notice_name,
1418 if($conf->exists('invoice_print_pdf')) {
1419 # Add the invoice to the current batch.
1420 $self->batch_invoice(\%opt);
1423 do_print $self->lpr_data(\%opt);
1427 =item fax_invoice HASHREF | [ TEMPLATE ]
1431 Options can be passed as a hashref (recommended) or as a single optional
1434 I<template>, if specified, is the name of a suffix for alternate invoices.
1436 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1442 my( $template, $notice_name );
1445 $template = $opt->{'template'} || '';
1446 $notice_name = $opt->{'notice_name'} || 'Invoice';
1448 $template = scalar(@_) ? shift : '';
1449 $notice_name = 'Invoice';
1452 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1453 unless $conf->exists('invoice_latex');
1455 my $dialstring = $self->cust_main->getfield('fax');
1459 'template' => $template,
1460 'notice_name' => $notice_name,
1463 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1464 'dialstring' => $dialstring,
1466 die $error if $error;
1470 =item batch_invoice [ HASHREF ]
1472 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1473 isn't an open batch, one will be created.
1478 my ($self, $opt) = @_;
1479 my $batch = FS::bill_batch->get_open_batch;
1480 my $cust_bill_batch = FS::cust_bill_batch->new({
1481 batchnum => $batch->batchnum,
1482 invnum => $self->invnum,
1484 return $cust_bill_batch->insert($opt);
1487 =item ftp_invoice [ TEMPLATENAME ]
1489 Sends this invoice data via FTP.
1491 TEMPLATENAME is unused?
1497 my $template = scalar(@_) ? shift : '';
1500 'protocol' => 'ftp',
1501 'server' => $conf->config('cust_bill-ftpserver'),
1502 'username' => $conf->config('cust_bill-ftpusername'),
1503 'password' => $conf->config('cust_bill-ftppassword'),
1504 'dir' => $conf->config('cust_bill-ftpdir'),
1505 'format' => $conf->config('cust_bill-ftpformat'),
1509 =item spool_invoice [ TEMPLATENAME ]
1511 Spools this invoice data (see L<FS::spool_csv>)
1513 TEMPLATENAME is unused?
1519 my $template = scalar(@_) ? shift : '';
1522 'format' => $conf->config('cust_bill-spoolformat'),
1523 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1527 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1529 Like B<send>, but only sends the invoice if it is the newest open invoice for
1534 sub send_if_newest {
1539 grep { $_->owed > 0 }
1540 qsearch('cust_bill', {
1541 'custnum' => $self->custnum,
1542 #'_date' => { op=>'>', value=>$self->_date },
1543 'invnum' => { op=>'>', value=>$self->invnum },
1550 =item send_csv OPTION => VALUE, ...
1552 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1556 protocol - currently only "ftp"
1562 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1563 and YYMMDDHHMMSS is a timestamp.
1565 See L</print_csv> for a description of the output format.
1570 my($self, %opt) = @_;
1574 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1575 mkdir $spooldir, 0700 unless -d $spooldir;
1577 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1578 my $file = "$spooldir/$tracctnum.csv";
1580 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1582 open(CSV, ">$file") or die "can't open $file: $!";
1590 if ( $opt{protocol} eq 'ftp' ) {
1591 eval "use Net::FTP;";
1593 $net = Net::FTP->new($opt{server}) or die @$;
1595 die "unknown protocol: $opt{protocol}";
1598 $net->login( $opt{username}, $opt{password} )
1599 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1601 $net->binary or die "can't set binary mode";
1603 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1605 $net->put($file) or die "can't put $file: $!";
1615 Spools CSV invoice data.
1621 =item format - 'default' or 'billco'
1623 =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>).
1625 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1627 =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.
1634 my($self, %opt) = @_;
1636 my $cust_main = $self->cust_main;
1638 if ( $opt{'dest'} ) {
1639 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1640 $cust_main->invoicing_list;
1641 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1642 || ! keys %invoicing_list;
1645 if ( $opt{'balanceover'} ) {
1647 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1650 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1651 mkdir $spooldir, 0700 unless -d $spooldir;
1653 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1657 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1658 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1661 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1663 open(CSV, ">>$file") or die "can't open $file: $!";
1664 flock(CSV, LOCK_EX);
1669 if ( lc($opt{'format'}) eq 'billco' ) {
1671 flock(CSV, LOCK_UN);
1676 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1679 open(CSV,">>$file") or die "can't open $file: $!";
1680 flock(CSV, LOCK_EX);
1686 flock(CSV, LOCK_UN);
1693 =item print_csv OPTION => VALUE, ...
1695 Returns CSV data for this invoice.
1699 format - 'default' or 'billco'
1701 Returns a list consisting of two scalars. The first is a single line of CSV
1702 header information for this invoice. The second is one or more lines of CSV
1703 detail information for this invoice.
1705 If I<format> is not specified or "default", the fields of the CSV file are as
1708 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1712 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1714 B<record_type> is C<cust_bill> for the initial header line only. The
1715 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1716 fields are filled in.
1718 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1719 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1722 =item invnum - invoice number
1724 =item custnum - customer number
1726 =item _date - invoice date
1728 =item charged - total invoice amount
1730 =item first - customer first name
1732 =item last - customer first name
1734 =item company - company name
1736 =item address1 - address line 1
1738 =item address2 - address line 1
1748 =item pkg - line item description
1750 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1752 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1754 =item sdate - start date for recurring fee
1756 =item edate - end date for recurring fee
1760 If I<format> is "billco", the fields of the header CSV file are as follows:
1762 +-------------------------------------------------------------------+
1763 | FORMAT HEADER FILE |
1764 |-------------------------------------------------------------------|
1765 | Field | Description | Name | Type | Width |
1766 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1767 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1768 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1769 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1770 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1771 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1772 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1773 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1774 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1775 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1776 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1777 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1778 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1779 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1780 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1781 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1782 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1783 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1784 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1785 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1786 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1787 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1788 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1789 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1790 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1791 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1792 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1793 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1794 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1795 +-------+-------------------------------+------------+------+-------+
1797 If I<format> is "billco", the fields of the detail CSV file are as follows:
1799 FORMAT FOR DETAIL FILE
1801 Field | Description | Name | Type | Width
1802 1 | N/A-Leave Empty | RC | CHAR | 2
1803 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1804 3 | Account Number | TRACCTNUM | CHAR | 15
1805 4 | Invoice Number | TRINVOICE | CHAR | 15
1806 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1807 6 | Transaction Detail | DETAILS | CHAR | 100
1808 7 | Amount | AMT | NUM* | 9
1809 8 | Line Format Control** | LNCTRL | CHAR | 2
1810 9 | Grouping Code | GROUP | CHAR | 2
1811 10 | User Defined | ACCT CODE | CHAR | 15
1816 my($self, %opt) = @_;
1818 eval "use Text::CSV_XS";
1821 my $cust_main = $self->cust_main;
1823 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1825 if ( lc($opt{'format'}) eq 'billco' ) {
1828 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1830 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1832 my( $previous_balance, @unused ) = $self->previous; #previous balance
1834 my $pmt_cr_applied = 0;
1835 $pmt_cr_applied += $_->{'amount'}
1836 foreach ( $self->_items_payments, $self->_items_credits ) ;
1838 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1841 '', # 1 | N/A-Leave Empty CHAR 2
1842 '', # 2 | N/A-Leave Empty CHAR 15
1843 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1844 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1845 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1846 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1847 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1848 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1849 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1850 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1851 '', # 10 | Ancillary Billing Information CHAR 30
1852 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1853 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1856 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1859 $duedate, # 14 | Bill Due Date CHAR 10
1861 $previous_balance, # 15 | Previous Balance NUM* 9
1862 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1863 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1864 $totaldue, # 18 | Total Amt Due NUM* 9
1865 $totaldue, # 19 | Total Amt Due NUM* 9
1866 '', # 20 | 30 Day Aging NUM* 9
1867 '', # 21 | 60 Day Aging NUM* 9
1868 '', # 22 | 90 Day Aging NUM* 9
1869 'N', # 23 | Y/N CHAR 1
1870 '', # 24 | Remittance automation CHAR 100
1871 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1872 $self->custnum, # 26 | Customer Reference Number CHAR 15
1873 '0', # 27 | Federal Tax*** NUM* 9
1874 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1875 '0', # 29 | Other Taxes & Fees*** NUM* 9
1884 time2str("%x", $self->_date),
1885 sprintf("%.2f", $self->charged),
1886 ( map { $cust_main->getfield($_) }
1887 qw( first last company address1 address2 city state zip country ) ),
1889 ) or die "can't create csv";
1892 my $header = $csv->string. "\n";
1895 if ( lc($opt{'format'}) eq 'billco' ) {
1898 foreach my $item ( $self->_items_pkg ) {
1901 '', # 1 | N/A-Leave Empty CHAR 2
1902 '', # 2 | N/A-Leave Empty CHAR 15
1903 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1904 $self->invnum, # 4 | Invoice Number CHAR 15
1905 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1906 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1907 $item->{'amount'}, # 7 | Amount NUM* 9
1908 '', # 8 | Line Format Control** CHAR 2
1909 '', # 9 | Grouping Code CHAR 2
1910 '', # 10 | User Defined CHAR 15
1913 $detail .= $csv->string. "\n";
1919 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1921 my($pkg, $setup, $recur, $sdate, $edate);
1922 if ( $cust_bill_pkg->pkgnum ) {
1924 ($pkg, $setup, $recur, $sdate, $edate) = (
1925 $cust_bill_pkg->part_pkg->pkg,
1926 ( $cust_bill_pkg->setup != 0
1927 ? sprintf("%.2f", $cust_bill_pkg->setup )
1929 ( $cust_bill_pkg->recur != 0
1930 ? sprintf("%.2f", $cust_bill_pkg->recur )
1932 ( $cust_bill_pkg->sdate
1933 ? time2str("%x", $cust_bill_pkg->sdate)
1935 ($cust_bill_pkg->edate
1936 ?time2str("%x", $cust_bill_pkg->edate)
1940 } else { #pkgnum tax
1941 next unless $cust_bill_pkg->setup != 0;
1942 $pkg = $cust_bill_pkg->desc;
1943 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1944 ( $sdate, $edate ) = ( '', '' );
1950 ( map { '' } (1..11) ),
1951 ($pkg, $setup, $recur, $sdate, $edate)
1952 ) or die "can't create csv";
1954 $detail .= $csv->string. "\n";
1960 ( $header, $detail );
1966 Pays this invoice with a compliemntary payment. If there is an error,
1967 returns the error, otherwise returns false.
1973 my $cust_pay = new FS::cust_pay ( {
1974 'invnum' => $self->invnum,
1975 'paid' => $self->owed,
1978 'payinfo' => $self->cust_main->payinfo,
1986 Attempts to pay this invoice with a credit card payment via a
1987 Business::OnlinePayment realtime gateway. See
1988 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1989 for supported processors.
1995 $self->realtime_bop( 'CC', @_ );
2000 Attempts to pay this invoice with an electronic check (ACH) payment via a
2001 Business::OnlinePayment realtime gateway. See
2002 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2003 for supported processors.
2009 $self->realtime_bop( 'ECHECK', @_ );
2014 Attempts to pay this invoice with phone bill (LEC) payment via a
2015 Business::OnlinePayment realtime gateway. See
2016 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2017 for supported processors.
2023 $self->realtime_bop( 'LEC', @_ );
2027 my( $self, $method ) = @_;
2029 my $cust_main = $self->cust_main;
2030 my $balance = $cust_main->balance;
2031 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2032 $amount = sprintf("%.2f", $amount);
2033 return "not run (balance $balance)" unless $amount > 0;
2035 my $description = 'Internet Services';
2036 if ( $conf->exists('business-onlinepayment-description') ) {
2037 my $dtempl = $conf->config('business-onlinepayment-description');
2039 my $agent_obj = $cust_main->agent
2040 or die "can't retreive agent for $cust_main (agentnum ".
2041 $cust_main->agentnum. ")";
2042 my $agent = $agent_obj->agent;
2043 my $pkgs = join(', ',
2044 map { $_->part_pkg->pkg }
2045 grep { $_->pkgnum } $self->cust_bill_pkg
2047 $description = eval qq("$dtempl");
2050 $cust_main->realtime_bop($method, $amount,
2051 'description' => $description,
2052 'invnum' => $self->invnum,
2053 #this didn't do what we want, it just calls apply_payments_and_credits
2055 'apply_to_invoice' => 1,
2057 #this changes application behavior: auto payments
2058 #triggered against a specific invoice are now applied
2059 #to that invoice instead of oldest open.
2065 =item batch_card OPTION => VALUE...
2067 Adds a payment for this invoice to the pending credit card batch (see
2068 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2069 runs the payment using a realtime gateway.
2074 my ($self, %options) = @_;
2075 my $cust_main = $self->cust_main;
2077 $options{invnum} = $self->invnum;
2079 $cust_main->batch_card(%options);
2082 sub _agent_template {
2084 $self->cust_main->agent_template;
2087 sub _agent_invoice_from {
2089 $self->cust_main->agent_invoice_from;
2092 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2094 Returns an text invoice, as a list of lines.
2096 Options can be passed as a hashref (recommended) or as a list of time, template
2097 and then any key/value pairs for any other options.
2099 I<time>, if specified, is used to control the printing of overdue messages. The
2100 default is now. It isn't the date of the invoice; that's the `_date' field.
2101 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2102 L<Time::Local> and L<Date::Parse> for conversion functions.
2104 I<template>, if specified, is the name of a suffix for alternate invoices.
2106 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2112 my( $today, $template, %opt );
2114 %opt = %{ shift() };
2115 $today = delete($opt{'time'}) || '';
2116 $template = delete($opt{template}) || '';
2118 ( $today, $template, %opt ) = @_;
2121 my %params = ( 'format' => 'template' );
2122 $params{'time'} = $today if $today;
2123 $params{'template'} = $template if $template;
2124 $params{$_} = $opt{$_}
2125 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2127 $self->print_generic( %params );
2130 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2132 Internal method - returns a filename of a filled-in LaTeX template for this
2133 invoice (Note: add ".tex" to get the actual filename), and a filename of
2134 an associated logo (with the .eps extension included).
2136 See print_ps and print_pdf for methods that return PostScript and PDF output.
2138 Options can be passed as a hashref (recommended) or as a list of time, template
2139 and then any key/value pairs for any other options.
2141 I<time>, if specified, is used to control the printing of overdue messages. The
2142 default is now. It isn't the date of the invoice; that's the `_date' field.
2143 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2144 L<Time::Local> and L<Date::Parse> for conversion functions.
2146 I<template>, if specified, is the name of a suffix for alternate invoices.
2148 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2154 my( $today, $template, %opt );
2156 %opt = %{ shift() };
2157 $today = delete($opt{'time'}) || '';
2158 $template = delete($opt{template}) || '';
2160 ( $today, $template, %opt ) = @_;
2163 my %params = ( 'format' => 'latex' );
2164 $params{'time'} = $today if $today;
2165 $params{'template'} = $template if $template;
2166 $params{$_} = $opt{$_}
2167 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2169 $template ||= $self->_agent_template;
2171 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2172 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2176 ) or die "can't open temp file: $!\n";
2178 my $agentnum = $self->cust_main->agentnum;
2180 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2181 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2182 or die "can't write temp file: $!\n";
2184 print $lh $conf->config_binary('logo.eps', $agentnum)
2185 or die "can't write temp file: $!\n";
2188 $params{'logo_file'} = $lh->filename;
2190 if($conf->exists('invoice-barcode')){
2191 my $png_file = $self->invoice_barcode($dir);
2192 my $eps_file = $png_file;
2193 $eps_file =~ s/\.png$/.eps/g;
2194 $png_file =~ /(barcode.*png)/;
2196 $eps_file =~ /(barcode.*eps)/;
2199 my $curr_dir = cwd();
2201 # after painfuly long experimentation, it was determined that sam2p won't
2202 # accept : and other chars in the path, no matter how hard I tried to
2203 # escape them, hence the chdir (and chdir back, just to be safe)
2204 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2205 or die "sam2p failed: $!\n";
2209 $params{'barcode_file'} = $eps_file;
2212 my @filled_in = $self->print_generic( %params );
2214 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2218 ) or die "can't open temp file: $!\n";
2219 print $fh join('', @filled_in );
2222 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2223 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2227 =item invoice_barcode DIR_OR_FALSE
2229 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2230 it is taken as the temp directory where the PNG file will be generated and the
2231 PNG file name is returned. Otherwise, the PNG image itself is returned.
2235 sub invoice_barcode {
2236 my ($self, $dir) = (shift,shift);
2238 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2239 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2240 my $gd = $gdbar->plot(Height => 30);
2243 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2247 ) or die "can't open temp file: $!\n";
2248 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2249 my $png_file = $bh->filename;
2256 =item print_generic OPTION => VALUE ...
2258 Internal method - returns a filled-in template for this invoice as a scalar.
2260 See print_ps and print_pdf for methods that return PostScript and PDF output.
2262 Non optional options include
2263 format - latex, html, template
2265 Optional options include
2267 template - a value used as a suffix for a configuration template
2269 time - a value used to control the printing of overdue messages. The
2270 default is now. It isn't the date of the invoice; that's the `_date' field.
2271 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2272 L<Time::Local> and L<Date::Parse> for conversion functions.
2276 unsquelch_cdr - overrides any per customer cdr squelching when true
2278 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2282 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2283 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2284 # yes: fixed width (dot matrix) text printing will be borked
2287 my( $self, %params ) = @_;
2288 my $today = $params{today} ? $params{today} : time;
2289 warn "$me print_generic called on $self with suffix $params{template}\n"
2292 my $format = $params{format};
2293 die "Unknown format: $format"
2294 unless $format =~ /^(latex|html|template)$/;
2296 my $cust_main = $self->cust_main;
2297 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2298 unless $cust_main->payname
2299 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2301 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2302 'html' => [ '<%=', '%>' ],
2303 'template' => [ '{', '}' ],
2306 warn "$me print_generic creating template\n"
2309 #create the template
2310 my $template = $params{template} ? $params{template} : $self->_agent_template;
2311 my $templatefile = "invoice_$format";
2312 $templatefile .= "_$template"
2313 if length($template);
2314 my @invoice_template = map "$_\n", $conf->config($templatefile)
2315 or die "cannot load config data $templatefile";
2318 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2319 #change this to a die when the old code is removed
2320 warn "old-style invoice template $templatefile; ".
2321 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2322 $old_latex = 'true';
2323 @invoice_template = _translate_old_latex_format(@invoice_template);
2326 warn "$me print_generic creating T:T object\n"
2329 my $text_template = new Text::Template(
2331 SOURCE => \@invoice_template,
2332 DELIMITERS => $delimiters{$format},
2335 warn "$me print_generic compiling T:T object\n"
2338 $text_template->compile()
2339 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2342 # additional substitution could possibly cause breakage in existing templates
2343 my %convert_maps = (
2345 'notes' => sub { map "$_", @_ },
2346 'footer' => sub { map "$_", @_ },
2347 'smallfooter' => sub { map "$_", @_ },
2348 'returnaddress' => sub { map "$_", @_ },
2349 'coupon' => sub { map "$_", @_ },
2350 'summary' => sub { map "$_", @_ },
2356 s/%%(.*)$/<!-- $1 -->/g;
2357 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2358 s/\\begin\{enumerate\}/<ol>/g;
2360 s/\\end\{enumerate\}/<\/ol>/g;
2361 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2370 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2372 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2377 s/\\\\\*?\s*$/<BR>/;
2378 s/\\hyphenation\{[\w\s\-]+}//;
2383 'coupon' => sub { "" },
2384 'summary' => sub { "" },
2391 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2392 s/\\begin\{enumerate\}//g;
2394 s/\\end\{enumerate\}//g;
2395 s/\\textbf\{(.*)\}/$1/g;
2402 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2404 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2409 s/\\\\\*?\s*$/\n/; # dubious
2410 s/\\hyphenation\{[\w\s\-]+}//;
2414 'coupon' => sub { "" },
2415 'summary' => sub { "" },
2420 # hashes for differing output formats
2421 my %nbsps = ( 'latex' => '~',
2422 'html' => '', # '&nbps;' would be nice
2423 'template' => '', # not used
2425 my $nbsp = $nbsps{$format};
2427 my %escape_functions = ( 'latex' => \&_latex_escape,
2428 'html' => \&_html_escape_nbsp,#\&encode_entities,
2429 'template' => sub { shift },
2431 my $escape_function = $escape_functions{$format};
2432 my $escape_function_nonbsp = ($format eq 'html')
2433 ? \&_html_escape : $escape_function;
2435 my %date_formats = ( 'latex' => $date_format_long,
2436 'html' => $date_format_long,
2439 $date_formats{'html'} =~ s/ / /g;
2441 my $date_format = $date_formats{$format};
2443 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2445 'html' => sub { return '<b>'. shift(). '</b>'
2447 'template' => sub { shift },
2449 my $embolden_function = $embolden_functions{$format};
2451 my %newline_tokens = ( 'latex' => '\\\\',
2455 my $newline_token = $newline_tokens{$format};
2457 warn "$me generating template variables\n"
2460 # generate template variables
2463 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2467 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2473 $returnaddress = join("\n",
2474 $conf->config_orbase("invoice_${format}returnaddress", $template)
2477 } elsif ( grep /\S/,
2478 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2480 my $convert_map = $convert_maps{$format}{'returnaddress'};
2483 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2488 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2490 my $convert_map = $convert_maps{$format}{'returnaddress'};
2491 $returnaddress = join( "\n", &$convert_map(
2492 map { s/( {2,})/'~' x length($1)/eg;
2496 ( $conf->config('company_name', $self->cust_main->agentnum),
2497 $conf->config('company_address', $self->cust_main->agentnum),
2504 my $warning = "Couldn't find a return address; ".
2505 "do you need to set the company_address configuration value?";
2507 $returnaddress = $nbsp;
2508 #$returnaddress = $warning;
2512 warn "$me generating invoice data\n"
2515 my $agentnum = $self->cust_main->agentnum;
2517 my %invoice_data = (
2520 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2521 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2522 'returnaddress' => $returnaddress,
2523 'agent' => &$escape_function($cust_main->agent->agent),
2526 'invnum' => $self->invnum,
2527 'date' => time2str($date_format, $self->_date),
2528 'today' => time2str($date_format_long, $today),
2529 'terms' => $self->terms,
2530 'template' => $template, #params{'template'},
2531 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2532 'current_charges' => sprintf("%.2f", $self->charged),
2533 'duedate' => $self->due_date2str($rdate_format), #date_format?
2536 'custnum' => $cust_main->display_custnum,
2537 'agent_custid' => &$escape_function($cust_main->agent_custid),
2538 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2539 payname company address1 address2 city state zip fax
2543 'ship_enable' => $conf->exists('invoice-ship_address'),
2544 'unitprices' => $conf->exists('invoice-unitprice'),
2545 'smallernotes' => $conf->exists('invoice-smallernotes'),
2546 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2547 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2549 #layout info -- would be fancy to calc some of this and bury the template
2551 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2552 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2553 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2554 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2555 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2556 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2557 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2558 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2559 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2560 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2562 # better hang on to conf_dir for a while (for old templates)
2563 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2565 #these are only used when doing paged plaintext
2571 my $min_sdate = 999999999999;
2573 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2574 next unless $cust_bill_pkg->pkgnum > 0;
2575 $min_sdate = $cust_bill_pkg->sdate
2576 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2577 $max_edate = $cust_bill_pkg->edate
2578 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2581 $invoice_data{'bill_period'} = '';
2582 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2583 . " to " . time2str('%e %h', $max_edate)
2584 if ($max_edate != 0 && $min_sdate != 999999999999);
2586 $invoice_data{finance_section} = '';
2587 if ( $conf->config('finance_pkgclass') ) {
2589 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2590 $invoice_data{finance_section} = $pkg_class->categoryname;
2592 $invoice_data{finance_amount} = '0.00';
2593 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2595 my $countrydefault = $conf->config('countrydefault') || 'US';
2596 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2597 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2598 my $method = $prefix.$_;
2599 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2601 $invoice_data{'ship_country'} = ''
2602 if ( $invoice_data{'ship_country'} eq $countrydefault );
2604 $invoice_data{'cid'} = $params{'cid'}
2607 if ( $cust_main->country eq $countrydefault ) {
2608 $invoice_data{'country'} = '';
2610 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2614 $invoice_data{'address'} = \@address;
2616 $cust_main->payname.
2617 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2618 ? " (P.O. #". $cust_main->payinfo. ")"
2622 push @address, $cust_main->company
2623 if $cust_main->company;
2624 push @address, $cust_main->address1;
2625 push @address, $cust_main->address2
2626 if $cust_main->address2;
2628 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2629 push @address, $invoice_data{'country'}
2630 if $invoice_data{'country'};
2632 while (scalar(@address) < 5);
2634 $invoice_data{'logo_file'} = $params{'logo_file'}
2635 if $params{'logo_file'};
2636 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2637 if $params{'barcode_file'};
2638 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2639 if $params{'barcode_img'};
2640 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2641 if $params{'barcode_cid'};
2643 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2644 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2645 #my $balance_due = $self->owed + $pr_total - $cr_total;
2646 my $balance_due = $self->owed + $pr_total;
2647 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2648 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2649 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2650 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2652 my $summarypage = '';
2653 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2656 $invoice_data{'summarypage'} = $summarypage;
2658 warn "$me substituting variables in notes, footer, smallfooter\n"
2661 foreach my $include (qw( notes footer smallfooter coupon )) {
2663 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2666 if ( $conf->exists($inc_file, $agentnum)
2667 && length( $conf->config($inc_file, $agentnum) ) ) {
2669 @inc_src = $conf->config($inc_file, $agentnum);
2673 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2675 my $convert_map = $convert_maps{$format}{$include};
2677 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2678 s/--\@\]/$delimiters{$format}[1]/g;
2681 &$convert_map( $conf->config($inc_file, $agentnum) );
2685 my $inc_tt = new Text::Template (
2687 SOURCE => [ map "$_\n", @inc_src ],
2688 DELIMITERS => $delimiters{$format},
2689 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2691 unless ( $inc_tt->compile() ) {
2692 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2693 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2697 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2699 $invoice_data{$include} =~ s/\n+$//
2700 if ($format eq 'latex');
2703 $invoice_data{'po_line'} =
2704 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2705 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2708 my %money_chars = ( 'latex' => '',
2709 'html' => $conf->config('money_char') || '$',
2712 my $money_char = $money_chars{$format};
2714 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2715 'html' => $conf->config('money_char') || '$',
2718 my $other_money_char = $other_money_chars{$format};
2719 $invoice_data{'dollar'} = $other_money_char;
2721 my @detail_items = ();
2722 my @total_items = ();
2726 $invoice_data{'detail_items'} = \@detail_items;
2727 $invoice_data{'total_items'} = \@total_items;
2728 $invoice_data{'buf'} = \@buf;
2729 $invoice_data{'sections'} = \@sections;
2731 warn "$me generating sections\n"
2734 my $previous_section = { 'description' => 'Previous Charges',
2735 'subtotal' => $other_money_char.
2736 sprintf('%.2f', $pr_total),
2737 'summarized' => $summarypage ? 'Y' : '',
2739 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2740 join(' / ', map { $cust_main->balance_date_range(@$_) }
2741 $self->_prior_month30s
2743 if $conf->exists('invoice_include_aging');
2746 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2747 'subtotal' => $taxtotal, # adjusted below
2748 'summarized' => $summarypage ? 'Y' : '',
2750 my $tax_weight = _pkg_category($tax_section->{description})
2751 ? _pkg_category($tax_section->{description})->weight
2753 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2754 $tax_section->{'sort_weight'} = $tax_weight;
2757 my $adjusttotal = 0;
2758 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2759 'subtotal' => 0, # adjusted below
2760 'summarized' => $summarypage ? 'Y' : '',
2762 my $adjust_weight = _pkg_category($adjust_section->{description})
2763 ? _pkg_category($adjust_section->{description})->weight
2765 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2766 $adjust_section->{'sort_weight'} = $adjust_weight;
2768 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2769 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2770 $invoice_data{'multisection'} = $multisection;
2771 my $late_sections = [];
2772 my $extra_sections = [];
2773 my $extra_lines = ();
2774 if ( $multisection ) {
2775 ($extra_sections, $extra_lines) =
2776 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2777 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2779 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2781 push @detail_items, @$extra_lines if $extra_lines;
2783 $self->_items_sections( $late_sections, # this could stand a refactor
2785 $escape_function_nonbsp,
2789 if ($conf->exists('svc_phone_sections')) {
2790 my ($phone_sections, $phone_lines) =
2791 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2792 push @{$late_sections}, @$phone_sections;
2793 push @detail_items, @$phone_lines;
2796 push @sections, { 'description' => '', 'subtotal' => '' };
2799 unless ( $conf->exists('disable_previous_balance')
2800 || $conf->exists('previous_balance-summary_only')
2804 warn "$me adding previous balances\n"
2807 foreach my $line_item ( $self->_items_previous ) {
2810 ext_description => [],
2812 $detail->{'ref'} = $line_item->{'pkgnum'};
2813 $detail->{'quantity'} = 1;
2814 $detail->{'section'} = $previous_section;
2815 $detail->{'description'} = &$escape_function($line_item->{'description'});
2816 if ( exists $line_item->{'ext_description'} ) {
2817 @{$detail->{'ext_description'}} = map {
2818 &$escape_function($_);
2819 } @{$line_item->{'ext_description'}};
2821 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2822 $line_item->{'amount'};
2823 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2825 push @detail_items, $detail;
2826 push @buf, [ $detail->{'description'},
2827 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2833 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2834 push @buf, ['','-----------'];
2835 push @buf, [ 'Total Previous Balance',
2836 $money_char. sprintf("%10.2f", $pr_total) ];
2840 if ( $conf->exists('svc_phone-did-summary') ) {
2841 warn "$me adding DID summary\n"
2844 my ($didsummary,$minutes) = $self->_did_summary;
2845 my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2847 { 'description' => $didsummary_desc,
2848 'ext_description' => [ $didsummary, $minutes ],
2853 foreach my $section (@sections, @$late_sections) {
2855 warn "$me adding section \n". Dumper($section)
2858 # begin some normalization
2859 $section->{'subtotal'} = $section->{'amount'}
2861 && !exists($section->{subtotal})
2862 && exists($section->{amount});
2864 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2865 if ( $invoice_data{finance_section} &&
2866 $section->{'description'} eq $invoice_data{finance_section} );
2868 $section->{'subtotal'} = $other_money_char.
2869 sprintf('%.2f', $section->{'subtotal'})
2872 # continue some normalization
2873 $section->{'amount'} = $section->{'subtotal'}
2877 if ( $section->{'description'} ) {
2878 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2883 warn "$me setting options\n"
2886 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2888 $options{'section'} = $section if $multisection;
2889 $options{'format'} = $format;
2890 $options{'escape_function'} = $escape_function;
2891 $options{'format_function'} = sub { () } unless $unsquelched;
2892 $options{'unsquelched'} = $unsquelched;
2893 $options{'summary_page'} = $summarypage;
2894 $options{'skip_usage'} =
2895 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2896 $options{'multilocation'} = $multilocation;
2897 $options{'multisection'} = $multisection;
2899 warn "$me searching for line items\n"
2902 foreach my $line_item ( $self->_items_pkg(%options) ) {
2904 warn "$me adding line item $line_item\n"
2908 ext_description => [],
2910 $detail->{'ref'} = $line_item->{'pkgnum'};
2911 $detail->{'quantity'} = $line_item->{'quantity'};
2912 $detail->{'section'} = $section;
2913 $detail->{'description'} = &$escape_function($line_item->{'description'});
2914 if ( exists $line_item->{'ext_description'} ) {
2915 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2917 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2918 $line_item->{'amount'};
2919 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2920 $line_item->{'unit_amount'};
2921 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2923 push @detail_items, $detail;
2924 push @buf, ( [ $detail->{'description'},
2925 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2927 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2931 if ( $section->{'description'} ) {
2932 push @buf, ( ['','-----------'],
2933 [ $section->{'description'}. ' sub-total',
2934 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2943 $invoice_data{current_less_finance} =
2944 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2946 if ( $multisection && !$conf->exists('disable_previous_balance')
2947 || $conf->exists('previous_balance-summary_only') )
2949 unshift @sections, $previous_section if $pr_total;
2952 warn "$me adding taxes\n"
2955 foreach my $tax ( $self->_items_tax ) {
2957 $taxtotal += $tax->{'amount'};
2959 my $description = &$escape_function( $tax->{'description'} );
2960 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2962 if ( $multisection ) {
2964 my $money = $old_latex ? '' : $money_char;
2965 push @detail_items, {
2966 ext_description => [],
2969 description => $description,
2970 amount => $money. $amount,
2972 section => $tax_section,
2977 push @total_items, {
2978 'total_item' => $description,
2979 'total_amount' => $other_money_char. $amount,
2984 push @buf,[ $description,
2985 $money_char. $amount,
2992 $total->{'total_item'} = 'Sub-total';
2993 $total->{'total_amount'} =
2994 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2996 if ( $multisection ) {
2997 $tax_section->{'subtotal'} = $other_money_char.
2998 sprintf('%.2f', $taxtotal);
2999 $tax_section->{'pretotal'} = 'New charges sub-total '.
3000 $total->{'total_amount'};
3001 push @sections, $tax_section if $taxtotal;
3003 unshift @total_items, $total;
3006 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3008 push @buf,['','-----------'];
3009 push @buf,[( $conf->exists('disable_previous_balance')
3011 : 'Total New Charges'
3013 $money_char. sprintf("%10.2f",$self->charged) ];
3019 $item = $conf->config('previous_balance-exclude_from_total')
3020 || 'Total New Charges'
3021 if $conf->exists('previous_balance-exclude_from_total');
3022 my $amount = $self->charged +
3023 ( $conf->exists('disable_previous_balance') ||
3024 $conf->exists('previous_balance-exclude_from_total')
3028 $total->{'total_item'} = &$embolden_function($item);
3029 $total->{'total_amount'} =
3030 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3031 if ( $multisection ) {
3032 if ( $adjust_section->{'sort_weight'} ) {
3033 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3034 sprintf("%.2f", ($self->billing_balance || 0) );
3036 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3037 sprintf('%.2f', $self->charged );
3040 push @total_items, $total;
3042 push @buf,['','-----------'];
3045 sprintf( '%10.2f', $amount )
3050 unless ( $conf->exists('disable_previous_balance') ) {
3051 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3054 my $credittotal = 0;
3055 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3058 $total->{'total_item'} = &$escape_function($credit->{'description'});
3059 $credittotal += $credit->{'amount'};
3060 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3061 $adjusttotal += $credit->{'amount'};
3062 if ( $multisection ) {
3063 my $money = $old_latex ? '' : $money_char;
3064 push @detail_items, {
3065 ext_description => [],
3068 description => &$escape_function($credit->{'description'}),
3069 amount => $money. $credit->{'amount'},
3071 section => $adjust_section,
3074 push @total_items, $total;
3078 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3081 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3082 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3086 my $paymenttotal = 0;
3087 foreach my $payment ( $self->_items_payments ) {
3089 $total->{'total_item'} = &$escape_function($payment->{'description'});
3090 $paymenttotal += $payment->{'amount'};
3091 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3092 $adjusttotal += $payment->{'amount'};
3093 if ( $multisection ) {
3094 my $money = $old_latex ? '' : $money_char;
3095 push @detail_items, {
3096 ext_description => [],
3099 description => &$escape_function($payment->{'description'}),
3100 amount => $money. $payment->{'amount'},
3102 section => $adjust_section,
3105 push @total_items, $total;
3107 push @buf, [ $payment->{'description'},
3108 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3111 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3113 if ( $multisection ) {
3114 $adjust_section->{'subtotal'} = $other_money_char.
3115 sprintf('%.2f', $adjusttotal);
3116 push @sections, $adjust_section
3117 unless $adjust_section->{sort_weight};
3122 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3123 $total->{'total_amount'} =
3124 &$embolden_function(
3125 $other_money_char. sprintf('%.2f', $summarypage
3127 $self->billing_balance
3128 : $self->owed + $pr_total
3131 if ( $multisection && !$adjust_section->{sort_weight} ) {
3132 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3133 $total->{'total_amount'};
3135 push @total_items, $total;
3137 push @buf,['','-----------'];
3138 push @buf,[$self->balance_due_msg, $money_char.
3139 sprintf("%10.2f", $balance_due ) ];
3142 if ( $conf->exists('previous_balance-show_credit')
3143 and $cust_main->balance < 0 ) {
3144 my $credit_total = {
3145 'total_item' => &$embolden_function($self->credit_balance_msg),
3146 'total_amount' => &$embolden_function(
3147 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3150 if ( $multisection ) {
3151 $adjust_section->{'posttotal'} .= $newline_token .
3152 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3155 push @total_items, $credit_total;
3157 push @buf,['','-----------'];
3158 push @buf,[$self->credit_balance_msg, $money_char.
3159 sprintf("%10.2f", -$cust_main->balance ) ];
3163 if ( $multisection ) {
3164 if ($conf->exists('svc_phone_sections')) {
3166 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3167 $total->{'total_amount'} =
3168 &$embolden_function(
3169 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3171 my $last_section = pop @sections;
3172 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3173 $total->{'total_amount'};
3174 push @sections, $last_section;
3176 push @sections, @$late_sections
3180 my @includelist = ();
3181 push @includelist, 'summary' if $summarypage;
3182 foreach my $include ( @includelist ) {
3184 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3187 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3189 @inc_src = $conf->config($inc_file, $agentnum);
3193 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3195 my $convert_map = $convert_maps{$format}{$include};
3197 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3198 s/--\@\]/$delimiters{$format}[1]/g;
3201 &$convert_map( $conf->config($inc_file, $agentnum) );
3205 my $inc_tt = new Text::Template (
3207 SOURCE => [ map "$_\n", @inc_src ],
3208 DELIMITERS => $delimiters{$format},
3209 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3211 unless ( $inc_tt->compile() ) {
3212 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3213 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3217 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3219 $invoice_data{$include} =~ s/\n+$//
3220 if ($format eq 'latex');
3225 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3226 /invoice_lines\((\d*)\)/;
3227 $invoice_lines += $1 || scalar(@buf);
3230 die "no invoice_lines() functions in template?"
3231 if ( $format eq 'template' && !$wasfunc );
3233 if ($format eq 'template') {
3235 if ( $invoice_lines ) {
3236 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3237 $invoice_data{'total_pages'}++
3238 if scalar(@buf) % $invoice_lines;
3241 #setup subroutine for the template
3242 sub FS::cust_bill::_template::invoice_lines {
3243 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3245 scalar(@FS::cust_bill::_template::buf)
3246 ? shift @FS::cust_bill::_template::buf
3255 push @collect, split("\n",
3256 $text_template->fill_in( HASH => \%invoice_data,
3257 PACKAGE => 'FS::cust_bill::_template'
3260 $FS::cust_bill::_template::page++;
3262 map "$_\n", @collect;
3264 warn "filling in template for invoice ". $self->invnum. "\n"
3266 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3269 $text_template->fill_in(HASH => \%invoice_data);
3273 # helper routine for generating date ranges
3274 sub _prior_month30s {
3277 [ 1, 2592000 ], # 0-30 days ago
3278 [ 2592000, 5184000 ], # 30-60 days ago
3279 [ 5184000, 7776000 ], # 60-90 days ago
3280 [ 7776000, 0 ], # 90+ days ago
3283 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3284 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3289 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3291 Returns an postscript invoice, as a scalar.
3293 Options can be passed as a hashref (recommended) or as a list of time, template
3294 and then any key/value pairs for any other options.
3296 I<time> an optional value used to control the printing of overdue messages. The
3297 default is now. It isn't the date of the invoice; that's the `_date' field.
3298 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3299 L<Time::Local> and L<Date::Parse> for conversion functions.
3301 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3308 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3309 my $ps = generate_ps($file);
3311 unlink($barcodefile);
3316 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3318 Returns an PDF invoice, as a scalar.
3320 Options can be passed as a hashref (recommended) or as a list of time, template
3321 and then any key/value pairs for any other options.
3323 I<time> an optional value used to control the printing of overdue messages. The
3324 default is now. It isn't the date of the invoice; that's the `_date' field.
3325 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3326 L<Time::Local> and L<Date::Parse> for conversion functions.
3328 I<template>, if specified, is the name of a suffix for alternate invoices.
3330 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3337 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3338 my $pdf = generate_pdf($file);
3340 unlink($barcodefile);
3345 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3347 Returns an HTML invoice, as a scalar.
3349 I<time> an optional value used to control the printing of overdue messages. The
3350 default is now. It isn't the date of the invoice; that's the `_date' field.
3351 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3352 L<Time::Local> and L<Date::Parse> for conversion functions.
3354 I<template>, if specified, is the name of a suffix for alternate invoices.
3356 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3358 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3359 when emailing the invoice as part of a multipart/related MIME email.
3367 %params = %{ shift() };
3369 $params{'time'} = shift;
3370 $params{'template'} = shift;
3371 $params{'cid'} = shift;
3374 $params{'format'} = 'html';
3376 $self->print_generic( %params );
3379 # quick subroutine for print_latex
3381 # There are ten characters that LaTeX treats as special characters, which
3382 # means that they do not simply typeset themselves:
3383 # # $ % & ~ _ ^ \ { }
3385 # TeX ignores blanks following an escaped character; if you want a blank (as
3386 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3390 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3391 $value =~ s/([<>])/\$$1\$/g;
3397 encode_entities($value);
3401 sub _html_escape_nbsp {
3402 my $value = _html_escape(shift);
3403 $value =~ s/ +/ /g;
3407 #utility methods for print_*
3409 sub _translate_old_latex_format {
3410 warn "_translate_old_latex_format called\n"
3417 if ( $line =~ /^%%Detail\s*$/ ) {
3419 push @template, q![@--!,
3420 q! foreach my $_tr_line (@detail_items) {!,
3421 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3422 q! $_tr_line->{'description'} .= !,
3423 q! "\\tabularnewline\n~~".!,
3424 q! join( "\\tabularnewline\n~~",!,
3425 q! @{$_tr_line->{'ext_description'}}!,
3429 while ( ( my $line_item_line = shift )
3430 !~ /^%%EndDetail\s*$/ ) {
3431 $line_item_line =~ s/'/\\'/g; # nice LTS
3432 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3433 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3434 push @template, " \$OUT .= '$line_item_line';";
3437 push @template, '}',
3440 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3442 push @template, '[@--',
3443 ' foreach my $_tr_line (@total_items) {';
3445 while ( ( my $total_item_line = shift )
3446 !~ /^%%EndTotalDetails\s*$/ ) {
3447 $total_item_line =~ s/'/\\'/g; # nice LTS
3448 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3449 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3450 push @template, " \$OUT .= '$total_item_line';";
3453 push @template, '}',
3457 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3458 push @template, $line;
3464 warn "$_\n" foreach @template;
3473 #check for an invoice-specific override
3474 return $self->invoice_terms if $self->invoice_terms;
3476 #check for a customer- specific override
3477 my $cust_main = $self->cust_main;
3478 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3480 #use configured default
3481 $conf->config('invoice_default_terms') || '';
3487 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3488 $duedate = $self->_date() + ( $1 * 86400 );
3495 $self->due_date ? time2str(shift, $self->due_date) : '';
3498 sub balance_due_msg {
3500 my $msg = 'Balance Due';
3501 return $msg unless $self->terms;
3502 if ( $self->due_date ) {
3503 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3504 } elsif ( $self->terms ) {
3505 $msg .= ' - '. $self->terms;
3510 sub balance_due_date {
3513 if ( $conf->exists('invoice_default_terms')
3514 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3515 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3520 sub credit_balance_msg { 'Credit Balance Remaining' }
3522 =item invnum_date_pretty
3524 Returns a string with the invoice number and date, for example:
3525 "Invoice #54 (3/20/2008)"
3529 sub invnum_date_pretty {
3531 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3536 Returns a string with the date, for example: "3/20/2008"
3542 time2str($date_format, $self->_date);
3545 use vars qw(%pkg_category_cache);
3546 sub _items_sections {
3549 my $summarypage = shift;
3551 my $extra_sections = shift;
3555 my %late_subtotal = ();
3558 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3561 my $usage = $cust_bill_pkg->usage;
3563 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3564 next if ( $display->summary && $summarypage );
3566 my $section = $display->section;
3567 my $type = $display->type;
3569 $not_tax{$section} = 1
3570 unless $cust_bill_pkg->pkgnum == 0;
3572 if ( $display->post_total && !$summarypage ) {
3573 if (! $type || $type eq 'S') {
3574 $late_subtotal{$section} += $cust_bill_pkg->setup
3575 if $cust_bill_pkg->setup != 0;
3579 $late_subtotal{$section} += $cust_bill_pkg->recur
3580 if $cust_bill_pkg->recur != 0;
3583 if ($type && $type eq 'R') {
3584 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3585 if $cust_bill_pkg->recur != 0;
3588 if ($type && $type eq 'U') {
3589 $late_subtotal{$section} += $usage
3590 unless scalar(@$extra_sections);
3595 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3597 if (! $type || $type eq 'S') {
3598 $subtotal{$section} += $cust_bill_pkg->setup
3599 if $cust_bill_pkg->setup != 0;
3603 $subtotal{$section} += $cust_bill_pkg->recur
3604 if $cust_bill_pkg->recur != 0;
3607 if ($type && $type eq 'R') {
3608 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3609 if $cust_bill_pkg->recur != 0;
3612 if ($type && $type eq 'U') {
3613 $subtotal{$section} += $usage
3614 unless scalar(@$extra_sections);
3623 %pkg_category_cache = ();
3625 push @$late, map { { 'description' => &{$escape}($_),
3626 'subtotal' => $late_subtotal{$_},
3628 'sort_weight' => ( _pkg_category($_)
3629 ? _pkg_category($_)->weight
3632 ((_pkg_category($_) && _pkg_category($_)->condense)
3633 ? $self->_condense_section($format)
3637 sort _sectionsort keys %late_subtotal;
3640 if ( $summarypage ) {
3641 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3642 map { $_->categoryname } qsearch('pkg_category', {});
3643 push @sections, '' if exists($subtotal{''});
3645 @sections = keys %subtotal;
3648 my @early = map { { 'description' => &{$escape}($_),
3649 'subtotal' => $subtotal{$_},
3650 'summarized' => $not_tax{$_} ? '' : 'Y',
3651 'tax_section' => $not_tax{$_} ? '' : 'Y',
3652 'sort_weight' => ( _pkg_category($_)
3653 ? _pkg_category($_)->weight
3656 ((_pkg_category($_) && _pkg_category($_)->condense)
3657 ? $self->_condense_section($format)
3662 push @early, @$extra_sections if $extra_sections;
3664 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3668 #helper subs for above
3671 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3675 my $categoryname = shift;
3676 $pkg_category_cache{$categoryname} ||=
3677 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3680 my %condensed_format = (
3681 'label' => [ qw( Description Qty Amount ) ],
3683 sub { shift->{description} },
3684 sub { shift->{quantity} },
3685 sub { my($href, %opt) = @_;
3686 ($opt{dollar} || ''). $href->{amount};
3689 'align' => [ qw( l r r ) ],
3690 'span' => [ qw( 5 1 1 ) ], # unitprices?
3691 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3694 sub _condense_section {
3695 my ( $self, $format ) = ( shift, shift );
3697 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3698 qw( description_generator
3701 total_line_generator
3706 sub _condensed_generator_defaults {
3707 my ( $self, $format ) = ( shift, shift );
3708 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3717 sub _condensed_header_generator {
3718 my ( $self, $format ) = ( shift, shift );
3720 my ( $f, $prefix, $suffix, $separator, $column ) =
3721 _condensed_generator_defaults($format);
3723 if ($format eq 'latex') {
3724 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3725 $suffix = "\\\\\n\\hline";
3728 sub { my ($d,$a,$s,$w) = @_;
3729 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3731 } elsif ( $format eq 'html' ) {
3732 $prefix = '<th></th>';
3736 sub { my ($d,$a,$s,$w) = @_;
3737 return qq!<th align="$html_align{$a}">$d</th>!;
3745 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3747 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3750 $prefix. join($separator, @result). $suffix;
3755 sub _condensed_description_generator {
3756 my ( $self, $format ) = ( shift, shift );
3758 my ( $f, $prefix, $suffix, $separator, $column ) =
3759 _condensed_generator_defaults($format);
3761 my $money_char = '$';
3762 if ($format eq 'latex') {
3763 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3765 $separator = " & \n";
3767 sub { my ($d,$a,$s,$w) = @_;
3768 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3770 $money_char = '\\dollar';
3771 }elsif ( $format eq 'html' ) {
3772 $prefix = '"><td align="center"></td>';
3776 sub { my ($d,$a,$s,$w) = @_;
3777 return qq!<td align="$html_align{$a}">$d</td>!;
3779 #$money_char = $conf->config('money_char') || '$';
3780 $money_char = ''; # this is madness
3788 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3790 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3792 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3793 map { $f->{$_}->[$i] } qw(align span width)
3797 $prefix. join( $separator, @result ). $suffix;
3802 sub _condensed_total_generator {
3803 my ( $self, $format ) = ( shift, shift );
3805 my ( $f, $prefix, $suffix, $separator, $column ) =
3806 _condensed_generator_defaults($format);
3809 if ($format eq 'latex') {
3812 $separator = " & \n";
3814 sub { my ($d,$a,$s,$w) = @_;
3815 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3817 }elsif ( $format eq 'html' ) {
3821 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3823 sub { my ($d,$a,$s,$w) = @_;
3824 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3833 # my $r = &{$f->{fields}->[$i]}(@args);
3834 # $r .= ' Total' unless $i;
3836 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3838 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3839 map { $f->{$_}->[$i] } qw(align span width)
3843 $prefix. join( $separator, @result ). $suffix;
3848 =item total_line_generator FORMAT
3850 Returns a coderef used for generation of invoice total line items for this
3851 usage_class. FORMAT is either html or latex
3855 # should not be used: will have issues with hash element names (description vs
3856 # total_item and amount vs total_amount -- another array of functions?
3858 sub _condensed_total_line_generator {
3859 my ( $self, $format ) = ( shift, shift );
3861 my ( $f, $prefix, $suffix, $separator, $column ) =
3862 _condensed_generator_defaults($format);
3865 if ($format eq 'latex') {
3868 $separator = " & \n";
3870 sub { my ($d,$a,$s,$w) = @_;
3871 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3873 }elsif ( $format eq 'html' ) {
3877 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3879 sub { my ($d,$a,$s,$w) = @_;
3880 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3889 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3891 &{$column}( &{$f->{fields}->[$i]}(@args),
3892 map { $f->{$_}->[$i] } qw(align span width)
3896 $prefix. join( $separator, @result ). $suffix;
3901 #sub _items_extra_usage_sections {
3903 # my $escape = shift;
3905 # my %sections = ();
3907 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3908 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3910 # next unless $cust_bill_pkg->pkgnum > 0;
3912 # foreach my $section ( keys %usage_class ) {
3914 # my $usage = $cust_bill_pkg->usage($section);
3916 # next unless $usage && $usage > 0;
3918 # $sections{$section} ||= 0;
3919 # $sections{$section} += $usage;
3925 # map { { 'description' => &{$escape}($_),
3926 # 'subtotal' => $sections{$_},
3927 # 'summarized' => '',
3928 # 'tax_section' => '',
3931 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3935 sub _items_extra_usage_sections {
3944 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3945 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3946 next unless $cust_bill_pkg->pkgnum > 0;
3948 foreach my $classnum ( keys %usage_class ) {
3949 my $section = $usage_class{$classnum}->classname;
3950 $classnums{$section} = $classnum;
3952 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3953 my $amount = $detail->amount;
3954 next unless $amount && $amount > 0;
3956 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3957 $sections{$section}{amount} += $amount; #subtotal
3958 $sections{$section}{calls}++;
3959 $sections{$section}{duration} += $detail->duration;
3961 my $desc = $detail->regionname;
3962 my $description = $desc;
3963 $description = substr($desc, 0, 50). '...'
3964 if $format eq 'latex' && length($desc) > 50;
3966 $lines{$section}{$desc} ||= {
3967 description => &{$escape}($description),
3968 #pkgpart => $part_pkg->pkgpart,
3969 pkgnum => $cust_bill_pkg->pkgnum,
3974 #unit_amount => $cust_bill_pkg->unitrecur,
3975 quantity => $cust_bill_pkg->quantity,
3976 product_code => 'N/A',
3977 ext_description => [],
3980 $lines{$section}{$desc}{amount} += $amount;
3981 $lines{$section}{$desc}{calls}++;
3982 $lines{$section}{$desc}{duration} += $detail->duration;
3988 my %sectionmap = ();
3989 foreach (keys %sections) {
3990 my $usage_class = $usage_class{$classnums{$_}};
3991 $sectionmap{$_} = { 'description' => &{$escape}($_),
3992 'amount' => $sections{$_}{amount}, #subtotal
3993 'calls' => $sections{$_}{calls},
3994 'duration' => $sections{$_}{duration},
3996 'tax_section' => '',
3997 'sort_weight' => $usage_class->weight,
3998 ( $usage_class->format
3999 ? ( map { $_ => $usage_class->$_($format) }
4000 qw( description_generator header_generator total_generator total_line_generator )
4007 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4011 foreach my $section ( keys %lines ) {
4012 foreach my $line ( keys %{$lines{$section}} ) {
4013 my $l = $lines{$section}{$line};
4014 $l->{section} = $sectionmap{$section};
4015 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4016 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4021 return(\@sections, \@lines);
4027 my $end = $self->_date;
4028 my $start = $end - 2592000; # 30 days
4029 my $cust_main = $self->cust_main;
4030 my @pkgs = $cust_main->all_pkgs;
4031 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4034 foreach my $pkg ( @pkgs ) {
4035 my @h_cust_svc = $pkg->h_cust_svc($end);
4036 foreach my $h_cust_svc ( @h_cust_svc ) {
4037 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4038 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4040 my $inserted = $h_cust_svc->date_inserted;
4041 my $deleted = $h_cust_svc->date_deleted;
4042 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4044 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4046 # DID either activated or ported in; cannot be both for same DID simultaneously
4047 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4048 && (!$phone_inserted->lnp_status
4049 || $phone_inserted->lnp_status eq ''
4050 || $phone_inserted->lnp_status eq 'native')) {
4053 else { # this one not so clean, should probably move to (h_)svc_phone
4054 my $phone_portedin = qsearchs( 'h_svc_phone',
4055 { 'svcnum' => $h_cust_svc->svcnum,
4056 'lnp_status' => 'portedin' },
4057 FS::h_svc_phone->sql_h_searchs($end),
4059 $num_portedin++ if $phone_portedin;
4062 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4063 if($deleted >= $start && $deleted <= $end && $phone_deleted
4064 && (!$phone_deleted->lnp_status
4065 || $phone_deleted->lnp_status ne 'portingout')) {
4068 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4069 && $phone_deleted->lnp_status
4070 && $phone_deleted->lnp_status eq 'portingout') {
4074 # increment usage minutes
4075 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4076 foreach my $cdr ( @cdrs ) {
4077 $minutes += $cdr->billsec/60;
4080 # don't look at this service again
4081 push @seen, $h_cust_svc->svcnum;
4085 $minutes = sprintf("%d", $minutes);
4086 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4087 . "$num_deactivated Ported-Out: $num_portedout ",
4088 "Total Minutes: $minutes");
4091 sub _items_svc_phone_sections {
4100 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4101 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4103 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4104 next unless $cust_bill_pkg->pkgnum > 0;
4106 my @header = $cust_bill_pkg->details_header;
4107 next unless scalar(@header);
4109 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4111 my $phonenum = $detail->phonenum;
4112 next unless $phonenum;
4114 my $amount = $detail->amount;
4115 next unless $amount && $amount > 0;
4117 $sections{$phonenum} ||= { 'amount' => 0,
4120 'sort_weight' => -1,
4121 'phonenum' => $phonenum,
4123 $sections{$phonenum}{amount} += $amount; #subtotal
4124 $sections{$phonenum}{calls}++;
4125 $sections{$phonenum}{duration} += $detail->duration;
4127 my $desc = $detail->regionname;
4128 my $description = $desc;
4129 $description = substr($desc, 0, 50). '...'
4130 if $format eq 'latex' && length($desc) > 50;
4132 $lines{$phonenum}{$desc} ||= {
4133 description => &{$escape}($description),
4134 #pkgpart => $part_pkg->pkgpart,
4142 product_code => 'N/A',
4143 ext_description => [],
4146 $lines{$phonenum}{$desc}{amount} += $amount;
4147 $lines{$phonenum}{$desc}{calls}++;
4148 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4150 my $line = $usage_class{$detail->classnum}->classname;
4151 $sections{"$phonenum $line"} ||=
4155 'sort_weight' => $usage_class{$detail->classnum}->weight,
4156 'phonenum' => $phonenum,
4157 'header' => [ @header ],
4159 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4160 $sections{"$phonenum $line"}{calls}++;
4161 $sections{"$phonenum $line"}{duration} += $detail->duration;
4163 $lines{"$phonenum $line"}{$desc} ||= {
4164 description => &{$escape}($description),
4165 #pkgpart => $part_pkg->pkgpart,
4173 product_code => 'N/A',
4174 ext_description => [],
4177 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4178 $lines{"$phonenum $line"}{$desc}{calls}++;
4179 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4180 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4181 $detail->formatted('format' => $format);
4186 my %sectionmap = ();
4187 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4188 foreach ( keys %sections ) {
4189 my @header = @{ $sections{$_}{header} || [] };
4191 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4192 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4193 my $usage_class = $summary ? $simple : $usage_simple;
4194 my $ending = $summary ? ' usage charges' : '';
4197 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4199 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4200 'amount' => $sections{$_}{amount}, #subtotal
4201 'calls' => $sections{$_}{calls},
4202 'duration' => $sections{$_}{duration},
4204 'tax_section' => '',
4205 'phonenum' => $sections{$_}{phonenum},
4206 'sort_weight' => $sections{$_}{sort_weight},
4207 'post_total' => $summary, #inspire pagebreak
4209 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4210 qw( description_generator
4213 total_line_generator
4220 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4221 $a->{sort_weight} <=> $b->{sort_weight}
4226 foreach my $section ( keys %lines ) {
4227 foreach my $line ( keys %{$lines{$section}} ) {
4228 my $l = $lines{$section}{$line};
4229 $l->{section} = $sectionmap{$section};
4230 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4231 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4236 if($conf->exists('phone_usage_class_summary')) {
4237 # this only works with Latex
4241 # after this, we'll have only two sections per DID:
4242 # Calls Summary and Calls Detail
4243 foreach my $section ( @sections ) {
4244 if($section->{'post_total'}) {
4245 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4246 $section->{'total_line_generator'} = sub { '' };
4247 $section->{'total_generator'} = sub { '' };
4248 $section->{'header_generator'} = sub { '' };
4249 $section->{'description_generator'} = '';
4250 push @newsections, $section;
4251 my %calls_detail = %$section;
4252 $calls_detail{'post_total'} = '';
4253 $calls_detail{'sort_weight'} = '';
4254 $calls_detail{'description_generator'} = sub { '' };
4255 $calls_detail{'header_generator'} = sub {
4256 return ' & Date/Time & Called Number & Duration & Price'
4257 if $format eq 'latex';
4260 $calls_detail{'description'} = 'Calls Detail: '
4261 . $section->{'phonenum'};
4262 push @newsections, \%calls_detail;
4266 # after this, each usage class is collapsed/summarized into a single
4267 # line under the Calls Summary section
4268 foreach my $newsection ( @newsections ) {
4269 if($newsection->{'post_total'}) { # this means Calls Summary
4270 foreach my $section ( @sections ) {
4271 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4272 && !$section->{'post_total'});
4273 my $newdesc = $section->{'description'};
4274 my $tn = $section->{'phonenum'};
4275 $newdesc =~ s/$tn//g;
4276 my $line = { ext_description => [],
4280 calls => $section->{'calls'},
4281 section => $newsection,
4282 duration => $section->{'duration'},
4283 description => $newdesc,
4284 amount => sprintf("%.2f",$section->{'amount'}),
4285 product_code => 'N/A',
4287 push @newlines, $line;
4292 # after this, Calls Details is populated with all CDRs
4293 foreach my $newsection ( @newsections ) {
4294 if(!$newsection->{'post_total'}) { # this means Calls Details
4295 foreach my $line ( @lines ) {
4296 next unless (scalar(@{$line->{'ext_description'}}) &&
4297 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4299 my @extdesc = @{$line->{'ext_description'}};
4301 foreach my $extdesc ( @extdesc ) {
4302 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4303 push @newextdesc, $extdesc;
4305 $line->{'ext_description'} = \@newextdesc;
4306 $line->{'section'} = $newsection;
4307 push @newlines, $line;
4312 return(\@newsections, \@newlines);
4315 return(\@sections, \@lines);
4322 #my @display = scalar(@_)
4324 # : qw( _items_previous _items_pkg );
4325 # #: qw( _items_pkg );
4326 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4327 my @display = qw( _items_previous _items_pkg );
4330 foreach my $display ( @display ) {
4331 push @b, $self->$display(@_);
4336 sub _items_previous {
4338 my $cust_main = $self->cust_main;
4339 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4341 foreach ( @pr_cust_bill ) {
4342 my $date = $conf->exists('invoice_show_prior_due_date')
4343 ? 'due '. $_->due_date2str($date_format)
4344 : time2str($date_format, $_->_date);
4346 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4347 #'pkgpart' => 'N/A',
4349 'amount' => sprintf("%.2f", $_->owed),
4355 # 'description' => 'Previous Balance',
4356 # #'pkgpart' => 'N/A',
4357 # 'pkgnum' => 'N/A',
4358 # 'amount' => sprintf("%10.2f", $pr_total ),
4359 # 'ext_description' => [ map {
4360 # "Invoice ". $_->invnum.
4361 # " (". time2str("%x",$_->_date). ") ".
4362 # sprintf("%10.2f", $_->owed)
4363 # } @pr_cust_bill ],
4372 warn "$me _items_pkg searching for all package line items\n"
4375 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4377 warn "$me _items_pkg filtering line items\n"
4379 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4381 if ($options{section} && $options{section}->{condensed}) {
4383 warn "$me _items_pkg condensing section\n"
4387 local $Storable::canonical = 1;
4388 foreach ( @items ) {
4390 delete $item->{ref};
4391 delete $item->{ext_description};
4392 my $key = freeze($item);
4393 $itemshash{$key} ||= 0;
4394 $itemshash{$key} ++; # += $item->{quantity};
4396 @items = sort { $a->{description} cmp $b->{description} }
4397 map { my $i = thaw($_);
4398 $i->{quantity} = $itemshash{$_};
4400 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4406 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4413 return 0 unless $a->itemdesc cmp $b->itemdesc;
4414 return -1 if $b->itemdesc eq 'Tax';
4415 return 1 if $a->itemdesc eq 'Tax';
4416 return -1 if $b->itemdesc eq 'Other surcharges';
4417 return 1 if $a->itemdesc eq 'Other surcharges';
4418 $a->itemdesc cmp $b->itemdesc;
4423 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4424 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4427 sub _items_cust_bill_pkg {
4429 my $cust_bill_pkgs = shift;
4432 my $format = $opt{format} || '';
4433 my $escape_function = $opt{escape_function} || sub { shift };
4434 my $format_function = $opt{format_function} || '';
4435 my $unsquelched = $opt{unsquelched} || '';
4436 my $section = $opt{section}->{description} if $opt{section};
4437 my $summary_page = $opt{summary_page} || '';
4438 my $multilocation = $opt{multilocation} || '';
4439 my $multisection = $opt{multisection} || '';
4440 my $discount_show_always = 0;
4443 my ($s, $r, $u) = ( undef, undef, undef );
4444 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4447 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4450 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4451 && $conf->exists('discount-show-always'));
4453 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4454 if ( $_ && !$cust_bill_pkg->hidden ) {
4455 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4456 $_->{amount} =~ s/^\-0\.00$/0.00/;
4457 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4459 unless ( $_->{amount} == 0 && !$discount_show_always );
4464 foreach my $display ( grep { defined($section)
4465 ? $_->section eq $section
4468 #grep { !$_->summary || !$summary_page } # bunk!
4469 grep { !$_->summary || $multisection }
4470 $cust_bill_pkg->cust_bill_pkg_display
4474 warn "$me _items_cust_bill_pkg considering display item $display\n"
4477 my $type = $display->type;
4479 my $desc = $cust_bill_pkg->desc;
4480 $desc = substr($desc, 0, 50). '...'
4481 if $format eq 'latex' && length($desc) > 50;
4483 my %details_opt = ( 'format' => $format,
4484 'escape_function' => $escape_function,
4485 'format_function' => $format_function,
4488 if ( $cust_bill_pkg->pkgnum > 0 ) {
4490 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4493 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4495 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4497 warn "$me _items_cust_bill_pkg adding setup\n"
4500 my $description = $desc;
4501 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4504 unless ( $cust_pkg->part_pkg->hide_svc_detail
4505 || $cust_bill_pkg->hidden )
4508 push @d, map &{$escape_function}($_),
4509 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4510 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4512 if ( $multilocation ) {
4513 my $loc = $cust_pkg->location_label;
4514 $loc = substr($loc, 0, 50). '...'
4515 if $format eq 'latex' && length($loc) > 50;
4516 push @d, &{$escape_function}($loc);
4521 push @d, $cust_bill_pkg->details(%details_opt)
4522 if $cust_bill_pkg->recur == 0;
4524 if ( $cust_bill_pkg->hidden ) {
4525 $s->{amount} += $cust_bill_pkg->setup;
4526 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4527 push @{ $s->{ext_description} }, @d;
4530 description => $description,
4531 #pkgpart => $part_pkg->pkgpart,
4532 pkgnum => $cust_bill_pkg->pkgnum,
4533 amount => $cust_bill_pkg->setup,
4534 unit_amount => $cust_bill_pkg->unitsetup,
4535 quantity => $cust_bill_pkg->quantity,
4536 ext_description => \@d,
4542 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4543 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4544 ( !$type || $type eq 'R' || $type eq 'U' )
4548 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4551 my $is_summary = $display->summary;
4552 my $description = ($is_summary && $type && $type eq 'U')
4553 ? "Usage charges" : $desc;
4555 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4556 " - ". time2str($date_format, $cust_bill_pkg->edate).
4558 unless $conf->exists('disable_line_item_date_ranges');
4562 #at least until cust_bill_pkg has "past" ranges in addition to
4563 #the "future" sdate/edate ones... see #3032
4564 my @dates = ( $self->_date );
4565 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4566 push @dates, $prev->sdate if $prev;
4567 push @dates, undef if !$prev;
4569 unless ( $cust_pkg->part_pkg->hide_svc_detail
4570 || $cust_bill_pkg->itemdesc
4571 || $cust_bill_pkg->hidden
4572 || $is_summary && $type && $type eq 'U' )
4575 warn "$me _items_cust_bill_pkg adding service details\n"
4578 push @d, map &{$escape_function}($_),
4579 $cust_pkg->h_labels_short(@dates, 'I')
4580 #$cust_bill_pkg->edate,
4581 #$cust_bill_pkg->sdate)
4582 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4584 warn "$me _items_cust_bill_pkg done adding service details\n"
4587 if ( $multilocation ) {
4588 my $loc = $cust_pkg->location_label;
4589 $loc = substr($loc, 0, 50). '...'
4590 if $format eq 'latex' && length($loc) > 50;
4591 push @d, &{$escape_function}($loc);
4596 unless ( $is_summary ) {
4597 warn "$me _items_cust_bill_pkg adding details\n"
4600 #instead of omitting details entirely in this case (unwanted side
4601 # effects), just omit CDRs
4602 $details_opt{'format_function'} = sub { () }
4603 if $type && $type eq 'R';
4605 push @d, $cust_bill_pkg->details(%details_opt);
4608 warn "$me _items_cust_bill_pkg calculating amount\n"
4613 $amount = $cust_bill_pkg->recur;
4614 } elsif ($type eq 'R') {
4615 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4616 } elsif ($type eq 'U') {
4617 $amount = $cust_bill_pkg->usage;
4620 if ( !$type || $type eq 'R' ) {
4622 warn "$me _items_cust_bill_pkg adding recur\n"
4625 if ( $cust_bill_pkg->hidden ) {
4626 $r->{amount} += $amount;
4627 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4628 push @{ $r->{ext_description} }, @d;
4631 description => $description,
4632 #pkgpart => $part_pkg->pkgpart,
4633 pkgnum => $cust_bill_pkg->pkgnum,
4635 unit_amount => $cust_bill_pkg->unitrecur,
4636 quantity => $cust_bill_pkg->quantity,
4637 ext_description => \@d,
4641 } else { # $type eq 'U'
4643 warn "$me _items_cust_bill_pkg adding usage\n"
4646 if ( $cust_bill_pkg->hidden ) {
4647 $u->{amount} += $amount;
4648 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4649 push @{ $u->{ext_description} }, @d;
4652 description => $description,
4653 #pkgpart => $part_pkg->pkgpart,
4654 pkgnum => $cust_bill_pkg->pkgnum,
4656 unit_amount => $cust_bill_pkg->unitrecur,
4657 quantity => $cust_bill_pkg->quantity,
4658 ext_description => \@d,
4664 } # recurring or usage with recurring charge
4666 } else { #pkgnum tax or one-shot line item (??)
4668 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4671 if ( $cust_bill_pkg->setup != 0 ) {
4673 'description' => $desc,
4674 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4677 if ( $cust_bill_pkg->recur != 0 ) {
4679 'description' => "$desc (".
4680 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4681 time2str($date_format, $cust_bill_pkg->edate). ')',
4682 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4692 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4695 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4697 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4698 $_->{amount} =~ s/^\-0\.00$/0.00/;
4699 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4701 unless ( $_->{amount} == 0 && !$discount_show_always );
4709 sub _items_credits {
4710 my( $self, %opt ) = @_;
4711 my $trim_len = $opt{'trim_len'} || 60;
4715 foreach ( $self->cust_credited ) {
4717 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4719 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4720 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4721 $reason = " ($reason) " if $reason;
4724 #'description' => 'Credit ref\#'. $_->crednum.
4725 # " (". time2str("%x",$_->cust_credit->_date) .")".
4727 'description' => 'Credit applied '.
4728 time2str($date_format,$_->cust_credit->_date). $reason,
4729 'amount' => sprintf("%.2f",$_->amount),
4737 sub _items_payments {
4741 #get & print payments
4742 foreach ( $self->cust_bill_pay ) {
4744 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4747 'description' => "Payment received ".
4748 time2str($date_format,$_->cust_pay->_date ),
4749 'amount' => sprintf("%.2f", $_->amount )
4757 =item call_details [ OPTION => VALUE ... ]
4759 Returns an array of CSV strings representing the call details for this invoice
4760 The only option available is the boolean prepend_billed_number
4765 my ($self, %opt) = @_;
4767 my $format_function = sub { shift };
4769 if ($opt{prepend_billed_number}) {
4770 $format_function = sub {
4774 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4779 my @details = map { $_->details( 'format_function' => $format_function,
4780 'escape_function' => sub{ return() },
4784 $self->cust_bill_pkg;
4785 my $header = $details[0];
4786 ( $header, grep { $_ ne $header } @details );
4796 =item process_reprint
4800 sub process_reprint {
4801 process_re_X('print', @_);
4804 =item process_reemail
4808 sub process_reemail {
4809 process_re_X('email', @_);
4817 process_re_X('fax', @_);
4825 process_re_X('ftp', @_);
4832 sub process_respool {
4833 process_re_X('spool', @_);
4836 use Storable qw(thaw);
4840 my( $method, $job ) = ( shift, shift );
4841 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4843 my $param = thaw(decode_base64(shift));
4844 warn Dumper($param) if $DEBUG;
4855 my($method, $job, %param ) = @_;
4857 warn "re_X $method for job $job with param:\n".
4858 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4861 #some false laziness w/search/cust_bill.html
4863 my $orderby = 'ORDER BY cust_bill._date';
4865 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4867 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4869 my @cust_bill = qsearch( {
4870 #'select' => "cust_bill.*",
4871 'table' => 'cust_bill',
4872 'addl_from' => $addl_from,
4874 'extra_sql' => $extra_sql,
4875 'order_by' => $orderby,
4879 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4881 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4884 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4885 foreach my $cust_bill ( @cust_bill ) {
4886 $cust_bill->$method();
4888 if ( $job ) { #progressbar foo
4890 if ( time - $min_sec > $last ) {
4891 my $error = $job->update_statustext(
4892 int( 100 * $num / scalar(@cust_bill) )
4894 die $error if $error;
4905 =head1 CLASS METHODS
4911 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4916 my ($class, $start, $end) = @_;
4918 $class->paid_sql($start, $end). ' - '.
4919 $class->credited_sql($start, $end);
4924 Returns an SQL fragment to retreive the net amount (charged minus credited).
4929 my ($class, $start, $end) = @_;
4930 'charged - '. $class->credited_sql($start, $end);
4935 Returns an SQL fragment to retreive the amount paid against this invoice.
4940 my ($class, $start, $end) = @_;
4941 $start &&= "AND cust_bill_pay._date <= $start";
4942 $end &&= "AND cust_bill_pay._date > $end";
4943 $start = '' unless defined($start);
4944 $end = '' unless defined($end);
4945 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4946 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4951 Returns an SQL fragment to retreive the amount credited against this invoice.
4956 my ($class, $start, $end) = @_;
4957 $start &&= "AND cust_credit_bill._date <= $start";
4958 $end &&= "AND cust_credit_bill._date > $end";
4959 $start = '' unless defined($start);
4960 $end = '' unless defined($end);
4961 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4962 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4967 Returns an SQL fragment to retrieve the due date of an invoice.
4968 Currently only supported on PostgreSQL.
4976 cust_bill.invoice_terms,
4977 cust_main.invoice_terms,
4978 \''.($conf->config('invoice_default_terms') || '').'\'
4979 ), E\'Net (\\\\d+)\'
4981 ) * 86400 + cust_bill._date'
4984 =item search_sql_where HASHREF
4986 Class method which returns an SQL WHERE fragment to search for parameters
4987 specified in HASHREF. Valid parameters are
4993 List reference of start date, end date, as UNIX timestamps.
5003 List reference of charged limits (exclusive).
5007 List reference of charged limits (exclusive).
5011 flag, return open invoices only
5015 flag, return net invoices only
5019 =item newest_percust
5023 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5027 sub search_sql_where {
5028 my($class, $param) = @_;
5030 warn "$me search_sql_where called with params: \n".
5031 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5037 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5038 push @search, "cust_main.agentnum = $1";
5042 if ( $param->{_date} ) {
5043 my($beginning, $ending) = @{$param->{_date}};
5045 push @search, "cust_bill._date >= $beginning",
5046 "cust_bill._date < $ending";
5050 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5051 push @search, "cust_bill.invnum >= $1";
5053 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5054 push @search, "cust_bill.invnum <= $1";
5058 if ( $param->{charged} ) {
5059 my @charged = ref($param->{charged})
5060 ? @{ $param->{charged} }
5061 : ($param->{charged});
5063 push @search, map { s/^charged/cust_bill.charged/; $_; }
5067 my $owed_sql = FS::cust_bill->owed_sql;
5070 if ( $param->{owed} ) {
5071 my @owed = ref($param->{owed})
5072 ? @{ $param->{owed} }
5074 push @search, map { s/^owed/$owed_sql/; $_; }
5079 push @search, "0 != $owed_sql"
5080 if $param->{'open'};
5081 push @search, '0 != '. FS::cust_bill->net_sql
5085 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5086 if $param->{'days'};
5089 if ( $param->{'newest_percust'} ) {
5091 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5092 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5094 my @newest_where = map { my $x = $_;
5095 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5098 grep ! /^cust_main./, @search;
5099 my $newest_where = scalar(@newest_where)
5100 ? ' AND '. join(' AND ', @newest_where)
5104 push @search, "cust_bill._date = (
5105 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5106 WHERE newest_cust_bill.custnum = cust_bill.custnum
5112 #agent virtualization
5113 my $curuser = $FS::CurrentUser::CurrentUser;
5114 if ( $curuser->username eq 'fs_queue'
5115 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5117 my $newuser = qsearchs('access_user', {
5118 'username' => $username,
5122 $curuser = $newuser;
5124 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5127 push @search, $curuser->agentnums_sql;
5129 join(' AND ', @search );
5141 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5142 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base