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(
251 foreach my $linked ( $self->$table() ) {
252 my $error = $linked->delete;
254 $dbh->rollback if $oldAutoCommit;
261 my $error = $self->SUPER::delete(@_);
263 $dbh->rollback if $oldAutoCommit;
267 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
273 =item replace [ OLD_RECORD ]
275 You can, but probably shouldn't modify invoices...
277 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
278 supplied, replaces this record. If there is an error, returns the error,
279 otherwise returns false.
283 #replace can be inherited from Record.pm
285 # replace_check is now the preferred way to #implement replace data checks
286 # (so $object->replace() works without an argument)
289 my( $new, $old ) = ( shift, shift );
290 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
291 #return "Can't change _date!" unless $old->_date eq $new->_date;
292 return "Can't change _date" unless $old->_date == $new->_date;
293 return "Can't change charged" unless $old->charged == $new->charged
294 || $old->charged == 0;
301 Checks all fields to make sure this is a valid invoice. If there is an error,
302 returns the error, otherwise returns false. Called by the insert and replace
311 $self->ut_numbern('invnum')
312 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
313 || $self->ut_numbern('_date')
314 || $self->ut_money('charged')
315 || $self->ut_numbern('printed')
316 || $self->ut_enum('closed', [ '', 'Y' ])
317 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
318 || $self->ut_numbern('agent_invid') #varchar?
320 return $error if $error;
322 $self->_date(time) unless $self->_date;
324 $self->printed(0) if $self->printed eq '';
331 Returns the displayed invoice number for this invoice: agent_invid if
332 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
338 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
339 return $self->agent_invid;
341 return $self->invnum;
347 Returns a list consisting of the total previous balance for this customer,
348 followed by the previous outstanding invoices (as FS::cust_bill objects also).
355 my @cust_bill = sort { $a->_date <=> $b->_date }
356 grep { $_->owed != 0 && $_->_date < $self->_date }
357 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
359 foreach ( @cust_bill ) { $total += $_->owed; }
365 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
372 { 'table' => 'cust_bill_pkg',
373 'hashref' => { 'invnum' => $self->invnum },
374 'order_by' => 'ORDER BY billpkgnum',
379 =item cust_bill_pkg_pkgnum PKGNUM
381 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
386 sub cust_bill_pkg_pkgnum {
387 my( $self, $pkgnum ) = @_;
389 { 'table' => 'cust_bill_pkg',
390 'hashref' => { 'invnum' => $self->invnum,
393 'order_by' => 'ORDER BY billpkgnum',
400 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
407 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
408 $self->cust_bill_pkg;
410 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
415 Returns true if any of the packages (or their definitions) corresponding to the
416 line items for this invoice have the no_auto flag set.
422 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
425 =item open_cust_bill_pkg
427 Returns the open line items for this invoice.
429 Note that cust_bill_pkg with both setup and recur fees are returned as two
430 separate line items, each with only one fee.
434 # modeled after cust_main::open_cust_bill
435 sub open_cust_bill_pkg {
438 # grep { $_->owed > 0 } $self->cust_bill_pkg
440 my %other = ( 'recur' => 'setup',
441 'setup' => 'recur', );
443 foreach my $field ( qw( recur setup )) {
444 push @open, map { $_->set( $other{$field}, 0 ); $_; }
445 grep { $_->owed($field) > 0 }
446 $self->cust_bill_pkg;
452 =item cust_bill_event
454 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
458 sub cust_bill_event {
460 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
463 =item num_cust_bill_event
465 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
469 sub num_cust_bill_event {
472 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
473 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
474 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
475 $sth->fetchrow_arrayref->[0];
480 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
484 #false laziness w/cust_pkg.pm
488 'table' => 'cust_event',
489 'addl_from' => 'JOIN part_event USING ( eventpart )',
490 'hashref' => { 'tablenum' => $self->invnum },
491 'extra_sql' => " AND eventtable = 'cust_bill' ",
497 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
501 #false laziness w/cust_pkg.pm
505 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
506 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
507 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
508 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
509 $sth->fetchrow_arrayref->[0];
514 Returns the customer (see L<FS::cust_main>) for this invoice.
520 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
523 =item cust_suspend_if_balance_over AMOUNT
525 Suspends the customer associated with this invoice if the total amount owed on
526 this invoice and all older invoices is greater than the specified amount.
528 Returns a list: an empty list on success or a list of errors.
532 sub cust_suspend_if_balance_over {
533 my( $self, $amount ) = ( shift, shift );
534 my $cust_main = $self->cust_main;
535 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
538 $cust_main->suspend(@_);
544 Depreciated. See the cust_credited method.
546 #Returns a list consisting of the total previous credited (see
547 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
548 #outstanding credits (FS::cust_credit objects).
554 croak "FS::cust_bill->cust_credit depreciated; see ".
555 "FS::cust_bill->cust_credit_bill";
558 #my @cust_credit = sort { $a->_date <=> $b->_date }
559 # grep { $_->credited != 0 && $_->_date < $self->_date }
560 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
562 #foreach (@cust_credit) { $total += $_->credited; }
563 #$total, @cust_credit;
568 Depreciated. See the cust_bill_pay method.
570 #Returns all payments (see L<FS::cust_pay>) for this invoice.
576 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
578 #sort { $a->_date <=> $b->_date }
579 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
585 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
588 sub cust_bill_pay_batch {
590 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
595 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
601 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
602 sort { $a->_date <=> $b->_date }
603 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
608 =item cust_credit_bill
610 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
616 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
617 sort { $a->_date <=> $b->_date }
618 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
622 sub cust_credit_bill {
623 shift->cust_credited(@_);
626 #=item cust_bill_pay_pkgnum PKGNUM
628 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
629 #with matching pkgnum.
633 #sub cust_bill_pay_pkgnum {
634 # my( $self, $pkgnum ) = @_;
635 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
636 # sort { $a->_date <=> $b->_date }
637 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
638 # 'pkgnum' => $pkgnum,
643 =item cust_bill_pay_pkg PKGNUM
645 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
646 applied against the matching pkgnum.
650 sub cust_bill_pay_pkg {
651 my( $self, $pkgnum ) = @_;
654 'select' => 'cust_bill_pay_pkg.*',
655 'table' => 'cust_bill_pay_pkg',
656 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
657 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
658 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
659 " AND cust_bill_pkg.pkgnum = $pkgnum",
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 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
703 " AND cust_bill_pkg.pkgnum = $pkgnum",
708 =item cust_bill_batch
710 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
714 sub cust_bill_batch {
716 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
721 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
728 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
730 foreach (@taxlines) { $total += $_->setup; }
736 Returns the amount owed (still outstanding) on this invoice, which is charged
737 minus all payment applications (see L<FS::cust_bill_pay>) and credit
738 applications (see L<FS::cust_credit_bill>).
744 my $balance = $self->charged;
745 $balance -= $_->amount foreach ( $self->cust_bill_pay );
746 $balance -= $_->amount foreach ( $self->cust_credited );
747 $balance = sprintf( "%.2f", $balance);
748 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
753 my( $self, $pkgnum ) = @_;
755 #my $balance = $self->charged;
757 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
759 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
760 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
762 $balance = sprintf( "%.2f", $balance);
763 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
767 =item apply_payments_and_credits [ OPTION => VALUE ... ]
769 Applies unapplied payments and credits to this invoice.
771 A hash of optional arguments may be passed. Currently "manual" is supported.
772 If true, a payment receipt is sent instead of a statement when
773 'payment_receipt_email' configuration option is set.
775 If there is an error, returns the error, otherwise returns false.
779 sub apply_payments_and_credits {
780 my( $self, %options ) = @_;
782 local $SIG{HUP} = 'IGNORE';
783 local $SIG{INT} = 'IGNORE';
784 local $SIG{QUIT} = 'IGNORE';
785 local $SIG{TERM} = 'IGNORE';
786 local $SIG{TSTP} = 'IGNORE';
787 local $SIG{PIPE} = 'IGNORE';
789 my $oldAutoCommit = $FS::UID::AutoCommit;
790 local $FS::UID::AutoCommit = 0;
793 $self->select_for_update; #mutex
795 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
796 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
798 if ( $conf->exists('pkg-balances') ) {
799 # limit @payments & @credits to those w/ a pkgnum grepped from $self
800 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
801 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
802 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
805 while ( $self->owed > 0 and ( @payments || @credits ) ) {
808 if ( @payments && @credits ) {
810 #decide which goes first by weight of top (unapplied) line item
812 my @open_lineitems = $self->open_cust_bill_pkg;
815 max( map { $_->part_pkg->pay_weight || 0 }
820 my $max_credit_weight =
821 max( map { $_->part_pkg->credit_weight || 0 }
827 #if both are the same... payments first? it has to be something
828 if ( $max_pay_weight >= $max_credit_weight ) {
834 } elsif ( @payments ) {
836 } elsif ( @credits ) {
839 die "guru meditation #12 and 35";
843 if ( $app eq 'pay' ) {
845 my $payment = shift @payments;
846 $unapp_amount = $payment->unapplied;
847 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
848 $app->pkgnum( $payment->pkgnum )
849 if $conf->exists('pkg-balances') && $payment->pkgnum;
851 } elsif ( $app eq 'credit' ) {
853 my $credit = shift @credits;
854 $unapp_amount = $credit->credited;
855 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
856 $app->pkgnum( $credit->pkgnum )
857 if $conf->exists('pkg-balances') && $credit->pkgnum;
860 die "guru meditation #12 and 35";
864 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
865 warn "owed_pkgnum ". $app->pkgnum;
866 $owed = $self->owed_pkgnum($app->pkgnum);
870 next unless $owed > 0;
872 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
873 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
875 $app->invnum( $self->invnum );
877 my $error = $app->insert(%options);
879 $dbh->rollback if $oldAutoCommit;
880 return "Error inserting ". $app->table. " record: $error";
882 die $error if $error;
886 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
891 =item generate_email OPTION => VALUE ...
899 sender address, required
903 alternate template name, optional
907 text attachment arrayref, optional
911 email subject, optional
915 notice name instead of "Invoice", optional
919 Returns an argument list to be passed to L<FS::Misc::send_email>.
930 my $me = '[FS::cust_bill::generate_email]';
933 'from' => $args{'from'},
934 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
938 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
939 'template' => $args{'template'},
940 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
943 my $cust_main = $self->cust_main;
945 if (ref($args{'to'}) eq 'ARRAY') {
946 $return{'to'} = $args{'to'};
948 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
949 $cust_main->invoicing_list
953 if ( $conf->exists('invoice_html') ) {
955 warn "$me creating HTML/text multipart message"
958 $return{'nobody'} = 1;
960 my $alternative = build MIME::Entity
961 'Type' => 'multipart/alternative',
962 'Encoding' => '7bit',
963 'Disposition' => 'inline'
967 if ( $conf->exists('invoice_email_pdf')
968 and scalar($conf->config('invoice_email_pdf_note')) ) {
970 warn "$me using 'invoice_email_pdf_note' in multipart message"
972 $data = [ map { $_ . "\n" }
973 $conf->config('invoice_email_pdf_note')
978 warn "$me not using 'invoice_email_pdf_note' in multipart message"
980 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
981 $data = $args{'print_text'};
983 $data = [ $self->print_text(\%opt) ];
988 $alternative->attach(
989 'Type' => 'text/plain',
990 #'Encoding' => 'quoted-printable',
991 'Encoding' => '7bit',
993 'Disposition' => 'inline',
996 $args{'from'} =~ /\@([\w\.\-]+)/;
997 my $from = $1 || 'example.com';
998 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1001 my $agentnum = $cust_main->agentnum;
1002 if ( defined($args{'template'}) && length($args{'template'})
1003 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1006 $logo = 'logo_'. $args{'template'}. '.png';
1010 my $image_data = $conf->config_binary( $logo, $agentnum);
1012 my $image = build MIME::Entity
1013 'Type' => 'image/png',
1014 'Encoding' => 'base64',
1015 'Data' => $image_data,
1016 'Filename' => 'logo.png',
1017 'Content-ID' => "<$content_id>",
1021 if($conf->exists('invoice-barcode')){
1022 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1023 $barcode = build MIME::Entity
1024 'Type' => 'image/png',
1025 'Encoding' => 'base64',
1026 'Data' => $self->invoice_barcode(0),
1027 'Filename' => 'barcode.png',
1028 'Content-ID' => "<$barcode_content_id>",
1030 $opt{'barcode_cid'} = $barcode_content_id;
1033 $alternative->attach(
1034 'Type' => 'text/html',
1035 'Encoding' => 'quoted-printable',
1036 'Data' => [ '<html>',
1039 ' '. encode_entities($return{'subject'}),
1042 ' <body bgcolor="#e8e8e8">',
1043 $self->print_html({ 'cid'=>$content_id, %opt }),
1047 'Disposition' => 'inline',
1048 #'Filename' => 'invoice.pdf',
1051 my @otherparts = ();
1052 if ( $cust_main->email_csv_cdr ) {
1054 push @otherparts, build MIME::Entity
1055 'Type' => 'text/csv',
1056 'Encoding' => '7bit',
1057 'Data' => [ map { "$_\n" }
1058 $self->call_details('prepend_billed_number' => 1)
1060 'Disposition' => 'attachment',
1061 'Filename' => 'usage-'. $self->invnum. '.csv',
1066 if ( $conf->exists('invoice_email_pdf') ) {
1071 # multipart/alternative
1077 my $related = build MIME::Entity 'Type' => 'multipart/related',
1078 'Encoding' => '7bit';
1080 #false laziness w/Misc::send_email
1081 $related->head->replace('Content-type',
1082 $related->mime_type.
1083 '; boundary="'. $related->head->multipart_boundary. '"'.
1084 '; type=multipart/alternative'
1087 $related->add_part($alternative);
1089 $related->add_part($image);
1091 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1093 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1097 #no other attachment:
1099 # multipart/alternative
1104 $return{'content-type'} = 'multipart/related';
1105 if($conf->exists('invoice-barcode')){
1106 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1109 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1111 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1112 #$return{'disposition'} = 'inline';
1118 if ( $conf->exists('invoice_email_pdf') ) {
1119 warn "$me creating PDF attachment"
1122 #mime parts arguments a la MIME::Entity->build().
1123 $return{'mimeparts'} = [
1124 { $self->mimebuild_pdf(\%opt) }
1128 if ( $conf->exists('invoice_email_pdf')
1129 and scalar($conf->config('invoice_email_pdf_note')) ) {
1131 warn "$me using 'invoice_email_pdf_note'"
1133 $return{'body'} = [ map { $_ . "\n" }
1134 $conf->config('invoice_email_pdf_note')
1139 warn "$me not using 'invoice_email_pdf_note'"
1141 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1142 $return{'body'} = $args{'print_text'};
1144 $return{'body'} = [ $self->print_text(\%opt) ];
1157 Returns a list suitable for passing to MIME::Entity->build(), representing
1158 this invoice as PDF attachment.
1165 'Type' => 'application/pdf',
1166 'Encoding' => 'base64',
1167 'Data' => [ $self->print_pdf(@_) ],
1168 'Disposition' => 'attachment',
1169 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1173 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1175 Sends this invoice to the destinations configured for this customer: sends
1176 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1178 Options can be passed as a hashref (recommended) or as a list of up to
1179 four values for templatename, agentnum, invoice_from and amount.
1181 I<template>, if specified, is the name of a suffix for alternate invoices.
1183 I<agentnum>, if specified, means that this invoice will only be sent for customers
1184 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1185 single agent) or an arrayref of agentnums.
1187 I<invoice_from>, if specified, overrides the default email invoice From: address.
1189 I<amount>, if specified, only sends the invoice if the total amount owed on this
1190 invoice and all older invoices is greater than the specified amount.
1192 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1196 sub queueable_send {
1199 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1200 or die "invalid invoice number: " . $opt{invnum};
1202 my @args = ( $opt{template}, $opt{agentnum} );
1203 push @args, $opt{invoice_from}
1204 if exists($opt{invoice_from}) && $opt{invoice_from};
1206 my $error = $self->send( @args );
1207 die $error if $error;
1214 my( $template, $invoice_from, $notice_name );
1216 my $balance_over = 0;
1220 $template = $opt->{'template'} || '';
1221 if ( $agentnums = $opt->{'agentnum'} ) {
1222 $agentnums = [ $agentnums ] unless ref($agentnums);
1224 $invoice_from = $opt->{'invoice_from'};
1225 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1226 $notice_name = $opt->{'notice_name'};
1228 $template = scalar(@_) ? shift : '';
1229 if ( scalar(@_) && $_[0] ) {
1230 $agentnums = ref($_[0]) ? shift : [ shift ];
1232 $invoice_from = shift if scalar(@_);
1233 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1236 return 'N/A' unless ! $agentnums
1237 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1240 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1242 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1243 $conf->config('invoice_from', $self->cust_main->agentnum );
1246 'template' => $template,
1247 'invoice_from' => $invoice_from,
1248 'notice_name' => ( $notice_name || 'Invoice' ),
1251 my @invoicing_list = $self->cust_main->invoicing_list;
1253 #$self->email_invoice(\%opt)
1255 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1257 #$self->print_invoice(\%opt)
1259 if grep { $_ eq 'POST' } @invoicing_list; #postal
1261 $self->fax_invoice(\%opt)
1262 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1268 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1270 Emails this invoice.
1272 Options can be passed as a hashref (recommended) or as a list of up to
1273 two values for templatename and invoice_from.
1275 I<template>, if specified, is the name of a suffix for alternate invoices.
1277 I<invoice_from>, if specified, overrides the default email invoice From: address.
1279 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1283 sub queueable_email {
1286 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1287 or die "invalid invoice number: " . $opt{invnum};
1289 my @args = ( $opt{template} );
1290 push @args, $opt{invoice_from}
1291 if exists($opt{invoice_from}) && $opt{invoice_from};
1293 my $error = $self->email( @args );
1294 die $error if $error;
1298 #sub email_invoice {
1302 my( $template, $invoice_from, $notice_name );
1305 $template = $opt->{'template'} || '';
1306 $invoice_from = $opt->{'invoice_from'};
1307 $notice_name = $opt->{'notice_name'} || 'Invoice';
1309 $template = scalar(@_) ? shift : '';
1310 $invoice_from = shift if scalar(@_);
1311 $notice_name = 'Invoice';
1314 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1315 $conf->config('invoice_from', $self->cust_main->agentnum );
1317 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1318 $self->cust_main->invoicing_list;
1320 if ( ! @invoicing_list ) { #no recipients
1321 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1322 die 'No recipients for customer #'. $self->custnum;
1324 #default: better to notify this person than silence
1325 @invoicing_list = ($invoice_from);
1329 my $subject = $self->email_subject($template);
1331 my $error = send_email(
1332 $self->generate_email(
1333 'from' => $invoice_from,
1334 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1335 'subject' => $subject,
1336 'template' => $template,
1337 'notice_name' => $notice_name,
1340 die "can't email invoice: $error\n" if $error;
1341 #die "$error\n" if $error;
1348 #my $template = scalar(@_) ? shift : '';
1351 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1354 my $cust_main = $self->cust_main;
1355 my $name = $cust_main->name;
1356 my $name_short = $cust_main->name_short;
1357 my $invoice_number = $self->invnum;
1358 my $invoice_date = $self->_date_pretty;
1360 eval qq("$subject");
1363 =item lpr_data HASHREF | [ TEMPLATE ]
1365 Returns the postscript or plaintext for this invoice as an arrayref.
1367 Options can be passed as a hashref (recommended) or as a single optional value
1370 I<template>, if specified, is the name of a suffix for alternate invoices.
1372 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1378 my( $template, $notice_name );
1381 $template = $opt->{'template'} || '';
1382 $notice_name = $opt->{'notice_name'} || 'Invoice';
1384 $template = scalar(@_) ? shift : '';
1385 $notice_name = 'Invoice';
1389 'template' => $template,
1390 'notice_name' => $notice_name,
1393 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1394 [ $self->$method( \%opt ) ];
1397 =item print HASHREF | [ TEMPLATE ]
1399 Prints this invoice.
1401 Options can be passed as a hashref (recommended) or as a single optional
1404 I<template>, if specified, is the name of a suffix for alternate invoices.
1406 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1410 #sub print_invoice {
1413 my( $template, $notice_name );
1416 $template = $opt->{'template'} || '';
1417 $notice_name = $opt->{'notice_name'} || 'Invoice';
1419 $template = scalar(@_) ? shift : '';
1420 $notice_name = 'Invoice';
1424 'template' => $template,
1425 'notice_name' => $notice_name,
1428 if($conf->exists('invoice_print_pdf')) {
1429 # Add the invoice to the current batch.
1430 $self->batch_invoice(\%opt);
1433 do_print $self->lpr_data(\%opt);
1437 =item fax_invoice HASHREF | [ TEMPLATE ]
1441 Options can be passed as a hashref (recommended) or as a single optional
1444 I<template>, if specified, is the name of a suffix for alternate invoices.
1446 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1452 my( $template, $notice_name );
1455 $template = $opt->{'template'} || '';
1456 $notice_name = $opt->{'notice_name'} || 'Invoice';
1458 $template = scalar(@_) ? shift : '';
1459 $notice_name = 'Invoice';
1462 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1463 unless $conf->exists('invoice_latex');
1465 my $dialstring = $self->cust_main->getfield('fax');
1469 'template' => $template,
1470 'notice_name' => $notice_name,
1473 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1474 'dialstring' => $dialstring,
1476 die $error if $error;
1480 =item batch_invoice [ HASHREF ]
1482 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1483 isn't an open batch, one will be created.
1488 my ($self, $opt) = @_;
1489 my $batch = FS::bill_batch->get_open_batch;
1490 my $cust_bill_batch = FS::cust_bill_batch->new({
1491 batchnum => $batch->batchnum,
1492 invnum => $self->invnum,
1494 return $cust_bill_batch->insert($opt);
1497 =item ftp_invoice [ TEMPLATENAME ]
1499 Sends this invoice data via FTP.
1501 TEMPLATENAME is unused?
1507 my $template = scalar(@_) ? shift : '';
1510 'protocol' => 'ftp',
1511 'server' => $conf->config('cust_bill-ftpserver'),
1512 'username' => $conf->config('cust_bill-ftpusername'),
1513 'password' => $conf->config('cust_bill-ftppassword'),
1514 'dir' => $conf->config('cust_bill-ftpdir'),
1515 'format' => $conf->config('cust_bill-ftpformat'),
1519 =item spool_invoice [ TEMPLATENAME ]
1521 Spools this invoice data (see L<FS::spool_csv>)
1523 TEMPLATENAME is unused?
1529 my $template = scalar(@_) ? shift : '';
1532 'format' => $conf->config('cust_bill-spoolformat'),
1533 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1537 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1539 Like B<send>, but only sends the invoice if it is the newest open invoice for
1544 sub send_if_newest {
1549 grep { $_->owed > 0 }
1550 qsearch('cust_bill', {
1551 'custnum' => $self->custnum,
1552 #'_date' => { op=>'>', value=>$self->_date },
1553 'invnum' => { op=>'>', value=>$self->invnum },
1560 =item send_csv OPTION => VALUE, ...
1562 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1566 protocol - currently only "ftp"
1572 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1573 and YYMMDDHHMMSS is a timestamp.
1575 See L</print_csv> for a description of the output format.
1580 my($self, %opt) = @_;
1584 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1585 mkdir $spooldir, 0700 unless -d $spooldir;
1587 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1588 my $file = "$spooldir/$tracctnum.csv";
1590 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1592 open(CSV, ">$file") or die "can't open $file: $!";
1600 if ( $opt{protocol} eq 'ftp' ) {
1601 eval "use Net::FTP;";
1603 $net = Net::FTP->new($opt{server}) or die @$;
1605 die "unknown protocol: $opt{protocol}";
1608 $net->login( $opt{username}, $opt{password} )
1609 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1611 $net->binary or die "can't set binary mode";
1613 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1615 $net->put($file) or die "can't put $file: $!";
1625 Spools CSV invoice data.
1631 =item format - 'default' or 'billco'
1633 =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>).
1635 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1637 =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.
1644 my($self, %opt) = @_;
1646 my $cust_main = $self->cust_main;
1648 if ( $opt{'dest'} ) {
1649 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1650 $cust_main->invoicing_list;
1651 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1652 || ! keys %invoicing_list;
1655 if ( $opt{'balanceover'} ) {
1657 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1660 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1661 mkdir $spooldir, 0700 unless -d $spooldir;
1663 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1667 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1668 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1671 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1673 open(CSV, ">>$file") or die "can't open $file: $!";
1674 flock(CSV, LOCK_EX);
1679 if ( lc($opt{'format'}) eq 'billco' ) {
1681 flock(CSV, LOCK_UN);
1686 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1689 open(CSV,">>$file") or die "can't open $file: $!";
1690 flock(CSV, LOCK_EX);
1696 flock(CSV, LOCK_UN);
1703 =item print_csv OPTION => VALUE, ...
1705 Returns CSV data for this invoice.
1709 format - 'default' or 'billco'
1711 Returns a list consisting of two scalars. The first is a single line of CSV
1712 header information for this invoice. The second is one or more lines of CSV
1713 detail information for this invoice.
1715 If I<format> is not specified or "default", the fields of the CSV file are as
1718 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1722 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1724 B<record_type> is C<cust_bill> for the initial header line only. The
1725 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1726 fields are filled in.
1728 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1729 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1732 =item invnum - invoice number
1734 =item custnum - customer number
1736 =item _date - invoice date
1738 =item charged - total invoice amount
1740 =item first - customer first name
1742 =item last - customer first name
1744 =item company - company name
1746 =item address1 - address line 1
1748 =item address2 - address line 1
1758 =item pkg - line item description
1760 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1762 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1764 =item sdate - start date for recurring fee
1766 =item edate - end date for recurring fee
1770 If I<format> is "billco", the fields of the header CSV file are as follows:
1772 +-------------------------------------------------------------------+
1773 | FORMAT HEADER FILE |
1774 |-------------------------------------------------------------------|
1775 | Field | Description | Name | Type | Width |
1776 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1777 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1778 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1779 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1780 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1781 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1782 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1783 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1784 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1785 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1786 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1787 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1788 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1789 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1790 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1791 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1792 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1793 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1794 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1795 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1796 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1797 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1798 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1799 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1800 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1801 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1802 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1803 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1804 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1805 +-------+-------------------------------+------------+------+-------+
1807 If I<format> is "billco", the fields of the detail CSV file are as follows:
1809 FORMAT FOR DETAIL FILE
1811 Field | Description | Name | Type | Width
1812 1 | N/A-Leave Empty | RC | CHAR | 2
1813 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1814 3 | Account Number | TRACCTNUM | CHAR | 15
1815 4 | Invoice Number | TRINVOICE | CHAR | 15
1816 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1817 6 | Transaction Detail | DETAILS | CHAR | 100
1818 7 | Amount | AMT | NUM* | 9
1819 8 | Line Format Control** | LNCTRL | CHAR | 2
1820 9 | Grouping Code | GROUP | CHAR | 2
1821 10 | User Defined | ACCT CODE | CHAR | 15
1826 my($self, %opt) = @_;
1828 eval "use Text::CSV_XS";
1831 my $cust_main = $self->cust_main;
1833 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1835 if ( lc($opt{'format'}) eq 'billco' ) {
1838 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1840 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1842 my( $previous_balance, @unused ) = $self->previous; #previous balance
1844 my $pmt_cr_applied = 0;
1845 $pmt_cr_applied += $_->{'amount'}
1846 foreach ( $self->_items_payments, $self->_items_credits ) ;
1848 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1851 '', # 1 | N/A-Leave Empty CHAR 2
1852 '', # 2 | N/A-Leave Empty CHAR 15
1853 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1854 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1855 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1856 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1857 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1858 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1859 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1860 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1861 '', # 10 | Ancillary Billing Information CHAR 30
1862 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1863 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1866 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1869 $duedate, # 14 | Bill Due Date CHAR 10
1871 $previous_balance, # 15 | Previous Balance NUM* 9
1872 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1873 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1874 $totaldue, # 18 | Total Amt Due NUM* 9
1875 $totaldue, # 19 | Total Amt Due NUM* 9
1876 '', # 20 | 30 Day Aging NUM* 9
1877 '', # 21 | 60 Day Aging NUM* 9
1878 '', # 22 | 90 Day Aging NUM* 9
1879 'N', # 23 | Y/N CHAR 1
1880 '', # 24 | Remittance automation CHAR 100
1881 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1882 $self->custnum, # 26 | Customer Reference Number CHAR 15
1883 '0', # 27 | Federal Tax*** NUM* 9
1884 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1885 '0', # 29 | Other Taxes & Fees*** NUM* 9
1894 time2str("%x", $self->_date),
1895 sprintf("%.2f", $self->charged),
1896 ( map { $cust_main->getfield($_) }
1897 qw( first last company address1 address2 city state zip country ) ),
1899 ) or die "can't create csv";
1902 my $header = $csv->string. "\n";
1905 if ( lc($opt{'format'}) eq 'billco' ) {
1908 foreach my $item ( $self->_items_pkg ) {
1911 '', # 1 | N/A-Leave Empty CHAR 2
1912 '', # 2 | N/A-Leave Empty CHAR 15
1913 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1914 $self->invnum, # 4 | Invoice Number CHAR 15
1915 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1916 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1917 $item->{'amount'}, # 7 | Amount NUM* 9
1918 '', # 8 | Line Format Control** CHAR 2
1919 '', # 9 | Grouping Code CHAR 2
1920 '', # 10 | User Defined CHAR 15
1923 $detail .= $csv->string. "\n";
1929 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1931 my($pkg, $setup, $recur, $sdate, $edate);
1932 if ( $cust_bill_pkg->pkgnum ) {
1934 ($pkg, $setup, $recur, $sdate, $edate) = (
1935 $cust_bill_pkg->part_pkg->pkg,
1936 ( $cust_bill_pkg->setup != 0
1937 ? sprintf("%.2f", $cust_bill_pkg->setup )
1939 ( $cust_bill_pkg->recur != 0
1940 ? sprintf("%.2f", $cust_bill_pkg->recur )
1942 ( $cust_bill_pkg->sdate
1943 ? time2str("%x", $cust_bill_pkg->sdate)
1945 ($cust_bill_pkg->edate
1946 ?time2str("%x", $cust_bill_pkg->edate)
1950 } else { #pkgnum tax
1951 next unless $cust_bill_pkg->setup != 0;
1952 $pkg = $cust_bill_pkg->desc;
1953 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1954 ( $sdate, $edate ) = ( '', '' );
1960 ( map { '' } (1..11) ),
1961 ($pkg, $setup, $recur, $sdate, $edate)
1962 ) or die "can't create csv";
1964 $detail .= $csv->string. "\n";
1970 ( $header, $detail );
1976 Pays this invoice with a compliemntary payment. If there is an error,
1977 returns the error, otherwise returns false.
1983 my $cust_pay = new FS::cust_pay ( {
1984 'invnum' => $self->invnum,
1985 'paid' => $self->owed,
1988 'payinfo' => $self->cust_main->payinfo,
1996 Attempts to pay this invoice with a credit card payment via a
1997 Business::OnlinePayment realtime gateway. See
1998 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1999 for supported processors.
2005 $self->realtime_bop( 'CC', @_ );
2010 Attempts to pay this invoice with an electronic check (ACH) payment via a
2011 Business::OnlinePayment realtime gateway. See
2012 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2013 for supported processors.
2019 $self->realtime_bop( 'ECHECK', @_ );
2024 Attempts to pay this invoice with phone bill (LEC) payment via a
2025 Business::OnlinePayment realtime gateway. See
2026 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2027 for supported processors.
2033 $self->realtime_bop( 'LEC', @_ );
2037 my( $self, $method ) = @_;
2039 my $cust_main = $self->cust_main;
2040 my $balance = $cust_main->balance;
2041 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2042 $amount = sprintf("%.2f", $amount);
2043 return "not run (balance $balance)" unless $amount > 0;
2045 my $description = 'Internet Services';
2046 if ( $conf->exists('business-onlinepayment-description') ) {
2047 my $dtempl = $conf->config('business-onlinepayment-description');
2049 my $agent_obj = $cust_main->agent
2050 or die "can't retreive agent for $cust_main (agentnum ".
2051 $cust_main->agentnum. ")";
2052 my $agent = $agent_obj->agent;
2053 my $pkgs = join(', ',
2054 map { $_->part_pkg->pkg }
2055 grep { $_->pkgnum } $self->cust_bill_pkg
2057 $description = eval qq("$dtempl");
2060 $cust_main->realtime_bop($method, $amount,
2061 'description' => $description,
2062 'invnum' => $self->invnum,
2063 #this didn't do what we want, it just calls apply_payments_and_credits
2065 'apply_to_invoice' => 1,
2067 #this changes application behavior: auto payments
2068 #triggered against a specific invoice are now applied
2069 #to that invoice instead of oldest open.
2075 =item batch_card OPTION => VALUE...
2077 Adds a payment for this invoice to the pending credit card batch (see
2078 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2079 runs the payment using a realtime gateway.
2084 my ($self, %options) = @_;
2085 my $cust_main = $self->cust_main;
2087 $options{invnum} = $self->invnum;
2089 $cust_main->batch_card(%options);
2092 sub _agent_template {
2094 $self->cust_main->agent_template;
2097 sub _agent_invoice_from {
2099 $self->cust_main->agent_invoice_from;
2102 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2104 Returns an text invoice, as a list of lines.
2106 Options can be passed as a hashref (recommended) or as a list of time, template
2107 and then any key/value pairs for any other options.
2109 I<time>, if specified, is used to control the printing of overdue messages. The
2110 default is now. It isn't the date of the invoice; that's the `_date' field.
2111 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2112 L<Time::Local> and L<Date::Parse> for conversion functions.
2114 I<template>, if specified, is the name of a suffix for alternate invoices.
2116 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2122 my( $today, $template, %opt );
2124 %opt = %{ shift() };
2125 $today = delete($opt{'time'}) || '';
2126 $template = delete($opt{template}) || '';
2128 ( $today, $template, %opt ) = @_;
2131 my %params = ( 'format' => 'template' );
2132 $params{'time'} = $today if $today;
2133 $params{'template'} = $template if $template;
2134 $params{$_} = $opt{$_}
2135 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2137 $self->print_generic( %params );
2140 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2142 Internal method - returns a filename of a filled-in LaTeX template for this
2143 invoice (Note: add ".tex" to get the actual filename), and a filename of
2144 an associated logo (with the .eps extension included).
2146 See print_ps and print_pdf for methods that return PostScript and PDF output.
2148 Options can be passed as a hashref (recommended) or as a list of time, template
2149 and then any key/value pairs for any other options.
2151 I<time>, if specified, is used to control the printing of overdue messages. The
2152 default is now. It isn't the date of the invoice; that's the `_date' field.
2153 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2154 L<Time::Local> and L<Date::Parse> for conversion functions.
2156 I<template>, if specified, is the name of a suffix for alternate invoices.
2158 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2164 my( $today, $template, %opt );
2166 %opt = %{ shift() };
2167 $today = delete($opt{'time'}) || '';
2168 $template = delete($opt{template}) || '';
2170 ( $today, $template, %opt ) = @_;
2173 my %params = ( 'format' => 'latex' );
2174 $params{'time'} = $today if $today;
2175 $params{'template'} = $template if $template;
2176 $params{$_} = $opt{$_}
2177 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2179 $template ||= $self->_agent_template;
2181 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2182 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2186 ) or die "can't open temp file: $!\n";
2188 my $agentnum = $self->cust_main->agentnum;
2190 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2191 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2192 or die "can't write temp file: $!\n";
2194 print $lh $conf->config_binary('logo.eps', $agentnum)
2195 or die "can't write temp file: $!\n";
2198 $params{'logo_file'} = $lh->filename;
2200 if($conf->exists('invoice-barcode')){
2201 my $png_file = $self->invoice_barcode($dir);
2202 my $eps_file = $png_file;
2203 $eps_file =~ s/\.png$/.eps/g;
2204 $png_file =~ /(barcode.*png)/;
2206 $eps_file =~ /(barcode.*eps)/;
2209 my $curr_dir = cwd();
2211 # after painfuly long experimentation, it was determined that sam2p won't
2212 # accept : and other chars in the path, no matter how hard I tried to
2213 # escape them, hence the chdir (and chdir back, just to be safe)
2214 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2215 or die "sam2p failed: $!\n";
2219 $params{'barcode_file'} = $eps_file;
2222 my @filled_in = $self->print_generic( %params );
2224 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2228 ) or die "can't open temp file: $!\n";
2229 print $fh join('', @filled_in );
2232 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2233 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2237 =item invoice_barcode DIR_OR_FALSE
2239 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2240 it is taken as the temp directory where the PNG file will be generated and the
2241 PNG file name is returned. Otherwise, the PNG image itself is returned.
2245 sub invoice_barcode {
2246 my ($self, $dir) = (shift,shift);
2248 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2249 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2250 my $gd = $gdbar->plot(Height => 30);
2253 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2257 ) or die "can't open temp file: $!\n";
2258 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2259 my $png_file = $bh->filename;
2266 =item print_generic OPTION => VALUE ...
2268 Internal method - returns a filled-in template for this invoice as a scalar.
2270 See print_ps and print_pdf for methods that return PostScript and PDF output.
2272 Non optional options include
2273 format - latex, html, template
2275 Optional options include
2277 template - a value used as a suffix for a configuration template
2279 time - a value used to control the printing of overdue messages. The
2280 default is now. It isn't the date of the invoice; that's the `_date' field.
2281 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2282 L<Time::Local> and L<Date::Parse> for conversion functions.
2286 unsquelch_cdr - overrides any per customer cdr squelching when true
2288 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2292 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2293 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2294 # yes: fixed width (dot matrix) text printing will be borked
2297 my( $self, %params ) = @_;
2298 my $today = $params{today} ? $params{today} : time;
2299 warn "$me print_generic called on $self with suffix $params{template}\n"
2302 my $format = $params{format};
2303 die "Unknown format: $format"
2304 unless $format =~ /^(latex|html|template)$/;
2306 my $cust_main = $self->cust_main;
2307 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2308 unless $cust_main->payname
2309 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2311 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2312 'html' => [ '<%=', '%>' ],
2313 'template' => [ '{', '}' ],
2316 warn "$me print_generic creating template\n"
2319 #create the template
2320 my $template = $params{template} ? $params{template} : $self->_agent_template;
2321 my $templatefile = "invoice_$format";
2322 $templatefile .= "_$template"
2323 if length($template);
2324 my @invoice_template = map "$_\n", $conf->config($templatefile)
2325 or die "cannot load config data $templatefile";
2328 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2329 #change this to a die when the old code is removed
2330 warn "old-style invoice template $templatefile; ".
2331 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2332 $old_latex = 'true';
2333 @invoice_template = _translate_old_latex_format(@invoice_template);
2336 warn "$me print_generic creating T:T object\n"
2339 my $text_template = new Text::Template(
2341 SOURCE => \@invoice_template,
2342 DELIMITERS => $delimiters{$format},
2345 warn "$me print_generic compiling T:T object\n"
2348 $text_template->compile()
2349 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2352 # additional substitution could possibly cause breakage in existing templates
2353 my %convert_maps = (
2355 'notes' => sub { map "$_", @_ },
2356 'footer' => sub { map "$_", @_ },
2357 'smallfooter' => sub { map "$_", @_ },
2358 'returnaddress' => sub { map "$_", @_ },
2359 'coupon' => sub { map "$_", @_ },
2360 'summary' => sub { map "$_", @_ },
2366 s/%%(.*)$/<!-- $1 -->/g;
2367 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2368 s/\\begin\{enumerate\}/<ol>/g;
2370 s/\\end\{enumerate\}/<\/ol>/g;
2371 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2380 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2382 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2387 s/\\\\\*?\s*$/<BR>/;
2388 s/\\hyphenation\{[\w\s\-]+}//;
2393 'coupon' => sub { "" },
2394 'summary' => sub { "" },
2401 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2402 s/\\begin\{enumerate\}//g;
2404 s/\\end\{enumerate\}//g;
2405 s/\\textbf\{(.*)\}/$1/g;
2412 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2414 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2419 s/\\\\\*?\s*$/\n/; # dubious
2420 s/\\hyphenation\{[\w\s\-]+}//;
2424 'coupon' => sub { "" },
2425 'summary' => sub { "" },
2430 # hashes for differing output formats
2431 my %nbsps = ( 'latex' => '~',
2432 'html' => '', # '&nbps;' would be nice
2433 'template' => '', # not used
2435 my $nbsp = $nbsps{$format};
2437 my %escape_functions = ( 'latex' => \&_latex_escape,
2438 'html' => \&_html_escape_nbsp,#\&encode_entities,
2439 'template' => sub { shift },
2441 my $escape_function = $escape_functions{$format};
2442 my $escape_function_nonbsp = ($format eq 'html')
2443 ? \&_html_escape : $escape_function;
2445 my %date_formats = ( 'latex' => $date_format_long,
2446 'html' => $date_format_long,
2449 $date_formats{'html'} =~ s/ / /g;
2451 my $date_format = $date_formats{$format};
2453 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2455 'html' => sub { return '<b>'. shift(). '</b>'
2457 'template' => sub { shift },
2459 my $embolden_function = $embolden_functions{$format};
2461 my %newline_tokens = ( 'latex' => '\\\\',
2465 my $newline_token = $newline_tokens{$format};
2467 warn "$me generating template variables\n"
2470 # generate template variables
2473 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2477 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2483 $returnaddress = join("\n",
2484 $conf->config_orbase("invoice_${format}returnaddress", $template)
2487 } elsif ( grep /\S/,
2488 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2490 my $convert_map = $convert_maps{$format}{'returnaddress'};
2493 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2498 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2500 my $convert_map = $convert_maps{$format}{'returnaddress'};
2501 $returnaddress = join( "\n", &$convert_map(
2502 map { s/( {2,})/'~' x length($1)/eg;
2506 ( $conf->config('company_name', $self->cust_main->agentnum),
2507 $conf->config('company_address', $self->cust_main->agentnum),
2514 my $warning = "Couldn't find a return address; ".
2515 "do you need to set the company_address configuration value?";
2517 $returnaddress = $nbsp;
2518 #$returnaddress = $warning;
2522 warn "$me generating invoice data\n"
2525 my $agentnum = $self->cust_main->agentnum;
2527 my %invoice_data = (
2530 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2531 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2532 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2533 'returnaddress' => $returnaddress,
2534 'agent' => &$escape_function($cust_main->agent->agent),
2537 'invnum' => $self->invnum,
2538 'date' => time2str($date_format, $self->_date),
2539 'today' => time2str($date_format_long, $today),
2540 'terms' => $self->terms,
2541 'template' => $template, #params{'template'},
2542 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2543 'current_charges' => sprintf("%.2f", $self->charged),
2544 'duedate' => $self->due_date2str($rdate_format), #date_format?
2547 'custnum' => $cust_main->display_custnum,
2548 'agent_custid' => &$escape_function($cust_main->agent_custid),
2549 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2550 payname company address1 address2 city state zip fax
2554 'ship_enable' => $conf->exists('invoice-ship_address'),
2555 'unitprices' => $conf->exists('invoice-unitprice'),
2556 'smallernotes' => $conf->exists('invoice-smallernotes'),
2557 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2558 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2560 #layout info -- would be fancy to calc some of this and bury the template
2562 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2563 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2564 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2565 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2566 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2567 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2568 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2569 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2570 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2571 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2573 # better hang on to conf_dir for a while (for old templates)
2574 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2576 #these are only used when doing paged plaintext
2582 my $min_sdate = 999999999999;
2584 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2585 next unless $cust_bill_pkg->pkgnum > 0;
2586 $min_sdate = $cust_bill_pkg->sdate
2587 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2588 $max_edate = $cust_bill_pkg->edate
2589 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2592 $invoice_data{'bill_period'} = '';
2593 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2594 . " to " . time2str('%e %h', $max_edate)
2595 if ($max_edate != 0 && $min_sdate != 999999999999);
2597 $invoice_data{finance_section} = '';
2598 if ( $conf->config('finance_pkgclass') ) {
2600 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2601 $invoice_data{finance_section} = $pkg_class->categoryname;
2603 $invoice_data{finance_amount} = '0.00';
2604 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2606 my $countrydefault = $conf->config('countrydefault') || 'US';
2607 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2608 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2609 my $method = $prefix.$_;
2610 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2612 $invoice_data{'ship_country'} = ''
2613 if ( $invoice_data{'ship_country'} eq $countrydefault );
2615 $invoice_data{'cid'} = $params{'cid'}
2618 if ( $cust_main->country eq $countrydefault ) {
2619 $invoice_data{'country'} = '';
2621 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2625 $invoice_data{'address'} = \@address;
2627 $cust_main->payname.
2628 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2629 ? " (P.O. #". $cust_main->payinfo. ")"
2633 push @address, $cust_main->company
2634 if $cust_main->company;
2635 push @address, $cust_main->address1;
2636 push @address, $cust_main->address2
2637 if $cust_main->address2;
2639 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2640 push @address, $invoice_data{'country'}
2641 if $invoice_data{'country'};
2643 while (scalar(@address) < 5);
2645 $invoice_data{'logo_file'} = $params{'logo_file'}
2646 if $params{'logo_file'};
2647 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2648 if $params{'barcode_file'};
2649 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2650 if $params{'barcode_img'};
2651 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2652 if $params{'barcode_cid'};
2654 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2655 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2656 #my $balance_due = $self->owed + $pr_total - $cr_total;
2657 my $balance_due = $self->owed + $pr_total;
2658 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2659 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2660 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2661 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2663 my $summarypage = '';
2664 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2667 $invoice_data{'summarypage'} = $summarypage;
2669 warn "$me substituting variables in notes, footer, smallfooter\n"
2672 foreach my $include (qw( notes footer smallfooter coupon )) {
2674 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2677 if ( $conf->exists($inc_file, $agentnum)
2678 && length( $conf->config($inc_file, $agentnum) ) ) {
2680 @inc_src = $conf->config($inc_file, $agentnum);
2684 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2686 my $convert_map = $convert_maps{$format}{$include};
2688 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2689 s/--\@\]/$delimiters{$format}[1]/g;
2692 &$convert_map( $conf->config($inc_file, $agentnum) );
2696 my $inc_tt = new Text::Template (
2698 SOURCE => [ map "$_\n", @inc_src ],
2699 DELIMITERS => $delimiters{$format},
2700 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2702 unless ( $inc_tt->compile() ) {
2703 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2704 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2708 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2710 $invoice_data{$include} =~ s/\n+$//
2711 if ($format eq 'latex');
2714 $invoice_data{'po_line'} =
2715 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2716 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2719 my %money_chars = ( 'latex' => '',
2720 'html' => $conf->config('money_char') || '$',
2723 my $money_char = $money_chars{$format};
2725 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2726 'html' => $conf->config('money_char') || '$',
2729 my $other_money_char = $other_money_chars{$format};
2730 $invoice_data{'dollar'} = $other_money_char;
2732 my @detail_items = ();
2733 my @total_items = ();
2737 $invoice_data{'detail_items'} = \@detail_items;
2738 $invoice_data{'total_items'} = \@total_items;
2739 $invoice_data{'buf'} = \@buf;
2740 $invoice_data{'sections'} = \@sections;
2742 warn "$me generating sections\n"
2745 my $previous_section = { 'description' => 'Previous Charges',
2746 'subtotal' => $other_money_char.
2747 sprintf('%.2f', $pr_total),
2748 'summarized' => $summarypage ? 'Y' : '',
2750 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2751 join(' / ', map { $cust_main->balance_date_range(@$_) }
2752 $self->_prior_month30s
2754 if $conf->exists('invoice_include_aging');
2757 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2758 'subtotal' => $taxtotal, # adjusted below
2759 'summarized' => $summarypage ? 'Y' : '',
2761 my $tax_weight = _pkg_category($tax_section->{description})
2762 ? _pkg_category($tax_section->{description})->weight
2764 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2765 $tax_section->{'sort_weight'} = $tax_weight;
2768 my $adjusttotal = 0;
2769 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2770 'subtotal' => 0, # adjusted below
2771 'summarized' => $summarypage ? 'Y' : '',
2773 my $adjust_weight = _pkg_category($adjust_section->{description})
2774 ? _pkg_category($adjust_section->{description})->weight
2776 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2777 $adjust_section->{'sort_weight'} = $adjust_weight;
2779 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2780 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2781 $invoice_data{'multisection'} = $multisection;
2782 my $late_sections = [];
2783 my $extra_sections = [];
2784 my $extra_lines = ();
2785 if ( $multisection ) {
2786 ($extra_sections, $extra_lines) =
2787 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2788 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2790 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2792 push @detail_items, @$extra_lines if $extra_lines;
2794 $self->_items_sections( $late_sections, # this could stand a refactor
2796 $escape_function_nonbsp,
2800 if ($conf->exists('svc_phone_sections')) {
2801 my ($phone_sections, $phone_lines) =
2802 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2803 push @{$late_sections}, @$phone_sections;
2804 push @detail_items, @$phone_lines;
2807 push @sections, { 'description' => '', 'subtotal' => '' };
2810 unless ( $conf->exists('disable_previous_balance')
2811 || $conf->exists('previous_balance-summary_only')
2815 warn "$me adding previous balances\n"
2818 foreach my $line_item ( $self->_items_previous ) {
2821 ext_description => [],
2823 $detail->{'ref'} = $line_item->{'pkgnum'};
2824 $detail->{'quantity'} = 1;
2825 $detail->{'section'} = $previous_section;
2826 $detail->{'description'} = &$escape_function($line_item->{'description'});
2827 if ( exists $line_item->{'ext_description'} ) {
2828 @{$detail->{'ext_description'}} = map {
2829 &$escape_function($_);
2830 } @{$line_item->{'ext_description'}};
2832 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2833 $line_item->{'amount'};
2834 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2836 push @detail_items, $detail;
2837 push @buf, [ $detail->{'description'},
2838 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2844 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2845 push @buf, ['','-----------'];
2846 push @buf, [ 'Total Previous Balance',
2847 $money_char. sprintf("%10.2f", $pr_total) ];
2851 if ( $conf->exists('svc_phone-did-summary') ) {
2852 warn "$me adding DID summary\n"
2855 my ($didsummary,$minutes) = $self->_did_summary;
2856 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2858 { 'description' => $didsummary_desc,
2859 'ext_description' => [ $didsummary, $minutes ],
2863 foreach my $section (@sections, @$late_sections) {
2865 warn "$me adding section \n". Dumper($section)
2868 # begin some normalization
2869 $section->{'subtotal'} = $section->{'amount'}
2871 && !exists($section->{subtotal})
2872 && exists($section->{amount});
2874 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2875 if ( $invoice_data{finance_section} &&
2876 $section->{'description'} eq $invoice_data{finance_section} );
2878 $section->{'subtotal'} = $other_money_char.
2879 sprintf('%.2f', $section->{'subtotal'})
2882 # continue some normalization
2883 $section->{'amount'} = $section->{'subtotal'}
2887 if ( $section->{'description'} ) {
2888 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2893 warn "$me setting options\n"
2896 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2898 $options{'section'} = $section if $multisection;
2899 $options{'format'} = $format;
2900 $options{'escape_function'} = $escape_function;
2901 $options{'format_function'} = sub { () } unless $unsquelched;
2902 $options{'unsquelched'} = $unsquelched;
2903 $options{'summary_page'} = $summarypage;
2904 $options{'skip_usage'} =
2905 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2906 $options{'multilocation'} = $multilocation;
2907 $options{'multisection'} = $multisection;
2909 warn "$me searching for line items\n"
2912 foreach my $line_item ( $self->_items_pkg(%options) ) {
2914 warn "$me adding line item $line_item\n"
2918 ext_description => [],
2920 $detail->{'ref'} = $line_item->{'pkgnum'};
2921 $detail->{'quantity'} = $line_item->{'quantity'};
2922 $detail->{'section'} = $section;
2923 $detail->{'description'} = &$escape_function($line_item->{'description'});
2924 if ( exists $line_item->{'ext_description'} ) {
2925 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2927 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2928 $line_item->{'amount'};
2929 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2930 $line_item->{'unit_amount'};
2931 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2933 push @detail_items, $detail;
2934 push @buf, ( [ $detail->{'description'},
2935 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2937 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2941 if ( $section->{'description'} ) {
2942 push @buf, ( ['','-----------'],
2943 [ $section->{'description'}. ' sub-total',
2944 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2953 $invoice_data{current_less_finance} =
2954 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2956 if ( $multisection && !$conf->exists('disable_previous_balance')
2957 || $conf->exists('previous_balance-summary_only') )
2959 unshift @sections, $previous_section if $pr_total;
2962 warn "$me adding taxes\n"
2965 foreach my $tax ( $self->_items_tax ) {
2967 $taxtotal += $tax->{'amount'};
2969 my $description = &$escape_function( $tax->{'description'} );
2970 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2972 if ( $multisection ) {
2974 my $money = $old_latex ? '' : $money_char;
2975 push @detail_items, {
2976 ext_description => [],
2979 description => $description,
2980 amount => $money. $amount,
2982 section => $tax_section,
2987 push @total_items, {
2988 'total_item' => $description,
2989 'total_amount' => $other_money_char. $amount,
2994 push @buf,[ $description,
2995 $money_char. $amount,
3002 $total->{'total_item'} = 'Sub-total';
3003 $total->{'total_amount'} =
3004 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3006 if ( $multisection ) {
3007 $tax_section->{'subtotal'} = $other_money_char.
3008 sprintf('%.2f', $taxtotal);
3009 $tax_section->{'pretotal'} = 'New charges sub-total '.
3010 $total->{'total_amount'};
3011 push @sections, $tax_section if $taxtotal;
3013 unshift @total_items, $total;
3016 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3018 push @buf,['','-----------'];
3019 push @buf,[( $conf->exists('disable_previous_balance')
3021 : 'Total New Charges'
3023 $money_char. sprintf("%10.2f",$self->charged) ];
3029 $item = $conf->config('previous_balance-exclude_from_total')
3030 || 'Total New Charges'
3031 if $conf->exists('previous_balance-exclude_from_total');
3032 my $amount = $self->charged +
3033 ( $conf->exists('disable_previous_balance') ||
3034 $conf->exists('previous_balance-exclude_from_total')
3038 $total->{'total_item'} = &$embolden_function($item);
3039 $total->{'total_amount'} =
3040 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3041 if ( $multisection ) {
3042 if ( $adjust_section->{'sort_weight'} ) {
3043 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3044 sprintf("%.2f", ($self->billing_balance || 0) );
3046 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3047 sprintf('%.2f', $self->charged );
3050 push @total_items, $total;
3052 push @buf,['','-----------'];
3055 sprintf( '%10.2f', $amount )
3060 unless ( $conf->exists('disable_previous_balance') ) {
3061 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3064 my $credittotal = 0;
3065 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3068 $total->{'total_item'} = &$escape_function($credit->{'description'});
3069 $credittotal += $credit->{'amount'};
3070 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3071 $adjusttotal += $credit->{'amount'};
3072 if ( $multisection ) {
3073 my $money = $old_latex ? '' : $money_char;
3074 push @detail_items, {
3075 ext_description => [],
3078 description => &$escape_function($credit->{'description'}),
3079 amount => $money. $credit->{'amount'},
3081 section => $adjust_section,
3084 push @total_items, $total;
3088 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3091 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3092 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3096 my $paymenttotal = 0;
3097 foreach my $payment ( $self->_items_payments ) {
3099 $total->{'total_item'} = &$escape_function($payment->{'description'});
3100 $paymenttotal += $payment->{'amount'};
3101 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3102 $adjusttotal += $payment->{'amount'};
3103 if ( $multisection ) {
3104 my $money = $old_latex ? '' : $money_char;
3105 push @detail_items, {
3106 ext_description => [],
3109 description => &$escape_function($payment->{'description'}),
3110 amount => $money. $payment->{'amount'},
3112 section => $adjust_section,
3115 push @total_items, $total;
3117 push @buf, [ $payment->{'description'},
3118 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3121 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3123 if ( $multisection ) {
3124 $adjust_section->{'subtotal'} = $other_money_char.
3125 sprintf('%.2f', $adjusttotal);
3126 push @sections, $adjust_section
3127 unless $adjust_section->{sort_weight};
3132 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3133 $total->{'total_amount'} =
3134 &$embolden_function(
3135 $other_money_char. sprintf('%.2f', $summarypage
3137 $self->billing_balance
3138 : $self->owed + $pr_total
3141 if ( $multisection && !$adjust_section->{sort_weight} ) {
3142 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3143 $total->{'total_amount'};
3145 push @total_items, $total;
3147 push @buf,['','-----------'];
3148 push @buf,[$self->balance_due_msg, $money_char.
3149 sprintf("%10.2f", $balance_due ) ];
3152 if ( $conf->exists('previous_balance-show_credit')
3153 and $cust_main->balance < 0 ) {
3154 my $credit_total = {
3155 'total_item' => &$embolden_function($self->credit_balance_msg),
3156 'total_amount' => &$embolden_function(
3157 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3160 if ( $multisection ) {
3161 $adjust_section->{'posttotal'} .= $newline_token .
3162 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3165 push @total_items, $credit_total;
3167 push @buf,['','-----------'];
3168 push @buf,[$self->credit_balance_msg, $money_char.
3169 sprintf("%10.2f", -$cust_main->balance ) ];
3173 if ( $multisection ) {
3174 if ($conf->exists('svc_phone_sections')) {
3176 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3177 $total->{'total_amount'} =
3178 &$embolden_function(
3179 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3181 my $last_section = pop @sections;
3182 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3183 $total->{'total_amount'};
3184 push @sections, $last_section;
3186 push @sections, @$late_sections
3190 my @includelist = ();
3191 push @includelist, 'summary' if $summarypage;
3192 foreach my $include ( @includelist ) {
3194 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3197 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3199 @inc_src = $conf->config($inc_file, $agentnum);
3203 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3205 my $convert_map = $convert_maps{$format}{$include};
3207 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3208 s/--\@\]/$delimiters{$format}[1]/g;
3211 &$convert_map( $conf->config($inc_file, $agentnum) );
3215 my $inc_tt = new Text::Template (
3217 SOURCE => [ map "$_\n", @inc_src ],
3218 DELIMITERS => $delimiters{$format},
3219 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3221 unless ( $inc_tt->compile() ) {
3222 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3223 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3227 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3229 $invoice_data{$include} =~ s/\n+$//
3230 if ($format eq 'latex');
3235 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3236 /invoice_lines\((\d*)\)/;
3237 $invoice_lines += $1 || scalar(@buf);
3240 die "no invoice_lines() functions in template?"
3241 if ( $format eq 'template' && !$wasfunc );
3243 if ($format eq 'template') {
3245 if ( $invoice_lines ) {
3246 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3247 $invoice_data{'total_pages'}++
3248 if scalar(@buf) % $invoice_lines;
3251 #setup subroutine for the template
3252 sub FS::cust_bill::_template::invoice_lines {
3253 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3255 scalar(@FS::cust_bill::_template::buf)
3256 ? shift @FS::cust_bill::_template::buf
3265 push @collect, split("\n",
3266 $text_template->fill_in( HASH => \%invoice_data,
3267 PACKAGE => 'FS::cust_bill::_template'
3270 $FS::cust_bill::_template::page++;
3272 map "$_\n", @collect;
3274 warn "filling in template for invoice ". $self->invnum. "\n"
3276 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3279 $text_template->fill_in(HASH => \%invoice_data);
3283 # helper routine for generating date ranges
3284 sub _prior_month30s {
3287 [ 1, 2592000 ], # 0-30 days ago
3288 [ 2592000, 5184000 ], # 30-60 days ago
3289 [ 5184000, 7776000 ], # 60-90 days ago
3290 [ 7776000, 0 ], # 90+ days ago
3293 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3294 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3299 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3301 Returns an postscript invoice, as a scalar.
3303 Options can be passed as a hashref (recommended) or as a list of time, template
3304 and then any key/value pairs for any other options.
3306 I<time> an optional value used to control the printing of overdue messages. The
3307 default is now. It isn't the date of the invoice; that's the `_date' field.
3308 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3309 L<Time::Local> and L<Date::Parse> for conversion functions.
3311 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3318 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3319 my $ps = generate_ps($file);
3321 unlink($barcodefile) if $barcodefile;
3326 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3328 Returns an PDF invoice, as a scalar.
3330 Options can be passed as a hashref (recommended) or as a list of time, template
3331 and then any key/value pairs for any other options.
3333 I<time> an optional value used to control the printing of overdue messages. The
3334 default is now. It isn't the date of the invoice; that's the `_date' field.
3335 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3336 L<Time::Local> and L<Date::Parse> for conversion functions.
3338 I<template>, if specified, is the name of a suffix for alternate invoices.
3340 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3347 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3348 my $pdf = generate_pdf($file);
3350 unlink($barcodefile) if $barcodefile;
3355 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3357 Returns an HTML invoice, as a scalar.
3359 I<time> an optional value used to control the printing of overdue messages. The
3360 default is now. It isn't the date of the invoice; that's the `_date' field.
3361 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3362 L<Time::Local> and L<Date::Parse> for conversion functions.
3364 I<template>, if specified, is the name of a suffix for alternate invoices.
3366 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3368 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3369 when emailing the invoice as part of a multipart/related MIME email.
3377 %params = %{ shift() };
3379 $params{'time'} = shift;
3380 $params{'template'} = shift;
3381 $params{'cid'} = shift;
3384 $params{'format'} = 'html';
3386 $self->print_generic( %params );
3389 # quick subroutine for print_latex
3391 # There are ten characters that LaTeX treats as special characters, which
3392 # means that they do not simply typeset themselves:
3393 # # $ % & ~ _ ^ \ { }
3395 # TeX ignores blanks following an escaped character; if you want a blank (as
3396 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3400 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3401 $value =~ s/([<>])/\$$1\$/g;
3407 encode_entities($value);
3411 sub _html_escape_nbsp {
3412 my $value = _html_escape(shift);
3413 $value =~ s/ +/ /g;
3417 #utility methods for print_*
3419 sub _translate_old_latex_format {
3420 warn "_translate_old_latex_format called\n"
3427 if ( $line =~ /^%%Detail\s*$/ ) {
3429 push @template, q![@--!,
3430 q! foreach my $_tr_line (@detail_items) {!,
3431 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3432 q! $_tr_line->{'description'} .= !,
3433 q! "\\tabularnewline\n~~".!,
3434 q! join( "\\tabularnewline\n~~",!,
3435 q! @{$_tr_line->{'ext_description'}}!,
3439 while ( ( my $line_item_line = shift )
3440 !~ /^%%EndDetail\s*$/ ) {
3441 $line_item_line =~ s/'/\\'/g; # nice LTS
3442 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3443 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3444 push @template, " \$OUT .= '$line_item_line';";
3447 push @template, '}',
3450 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3452 push @template, '[@--',
3453 ' foreach my $_tr_line (@total_items) {';
3455 while ( ( my $total_item_line = shift )
3456 !~ /^%%EndTotalDetails\s*$/ ) {
3457 $total_item_line =~ s/'/\\'/g; # nice LTS
3458 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3459 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3460 push @template, " \$OUT .= '$total_item_line';";
3463 push @template, '}',
3467 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3468 push @template, $line;
3474 warn "$_\n" foreach @template;
3483 #check for an invoice-specific override
3484 return $self->invoice_terms if $self->invoice_terms;
3486 #check for a customer- specific override
3487 my $cust_main = $self->cust_main;
3488 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3490 #use configured default
3491 $conf->config('invoice_default_terms') || '';
3497 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3498 $duedate = $self->_date() + ( $1 * 86400 );
3505 $self->due_date ? time2str(shift, $self->due_date) : '';
3508 sub balance_due_msg {
3510 my $msg = 'Balance Due';
3511 return $msg unless $self->terms;
3512 if ( $self->due_date ) {
3513 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3514 } elsif ( $self->terms ) {
3515 $msg .= ' - '. $self->terms;
3520 sub balance_due_date {
3523 if ( $conf->exists('invoice_default_terms')
3524 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3525 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3530 sub credit_balance_msg { 'Credit Balance Remaining' }
3532 =item invnum_date_pretty
3534 Returns a string with the invoice number and date, for example:
3535 "Invoice #54 (3/20/2008)"
3539 sub invnum_date_pretty {
3541 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3546 Returns a string with the date, for example: "3/20/2008"
3552 time2str($date_format, $self->_date);
3555 use vars qw(%pkg_category_cache);
3556 sub _items_sections {
3559 my $summarypage = shift;
3561 my $extra_sections = shift;
3565 my %late_subtotal = ();
3568 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3571 my $usage = $cust_bill_pkg->usage;
3573 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3574 next if ( $display->summary && $summarypage );
3576 my $section = $display->section;
3577 my $type = $display->type;
3579 $not_tax{$section} = 1
3580 unless $cust_bill_pkg->pkgnum == 0;
3582 if ( $display->post_total && !$summarypage ) {
3583 if (! $type || $type eq 'S') {
3584 $late_subtotal{$section} += $cust_bill_pkg->setup
3585 if $cust_bill_pkg->setup != 0;
3589 $late_subtotal{$section} += $cust_bill_pkg->recur
3590 if $cust_bill_pkg->recur != 0;
3593 if ($type && $type eq 'R') {
3594 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3595 if $cust_bill_pkg->recur != 0;
3598 if ($type && $type eq 'U') {
3599 $late_subtotal{$section} += $usage
3600 unless scalar(@$extra_sections);
3605 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3607 if (! $type || $type eq 'S') {
3608 $subtotal{$section} += $cust_bill_pkg->setup
3609 if $cust_bill_pkg->setup != 0;
3613 $subtotal{$section} += $cust_bill_pkg->recur
3614 if $cust_bill_pkg->recur != 0;
3617 if ($type && $type eq 'R') {
3618 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3619 if $cust_bill_pkg->recur != 0;
3622 if ($type && $type eq 'U') {
3623 $subtotal{$section} += $usage
3624 unless scalar(@$extra_sections);
3633 %pkg_category_cache = ();
3635 push @$late, map { { 'description' => &{$escape}($_),
3636 'subtotal' => $late_subtotal{$_},
3638 'sort_weight' => ( _pkg_category($_)
3639 ? _pkg_category($_)->weight
3642 ((_pkg_category($_) && _pkg_category($_)->condense)
3643 ? $self->_condense_section($format)
3647 sort _sectionsort keys %late_subtotal;
3650 if ( $summarypage ) {
3651 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3652 map { $_->categoryname } qsearch('pkg_category', {});
3653 push @sections, '' if exists($subtotal{''});
3655 @sections = keys %subtotal;
3658 my @early = map { { 'description' => &{$escape}($_),
3659 'subtotal' => $subtotal{$_},
3660 'summarized' => $not_tax{$_} ? '' : 'Y',
3661 'tax_section' => $not_tax{$_} ? '' : 'Y',
3662 'sort_weight' => ( _pkg_category($_)
3663 ? _pkg_category($_)->weight
3666 ((_pkg_category($_) && _pkg_category($_)->condense)
3667 ? $self->_condense_section($format)
3672 push @early, @$extra_sections if $extra_sections;
3674 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3678 #helper subs for above
3681 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3685 my $categoryname = shift;
3686 $pkg_category_cache{$categoryname} ||=
3687 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3690 my %condensed_format = (
3691 'label' => [ qw( Description Qty Amount ) ],
3693 sub { shift->{description} },
3694 sub { shift->{quantity} },
3695 sub { my($href, %opt) = @_;
3696 ($opt{dollar} || ''). $href->{amount};
3699 'align' => [ qw( l r r ) ],
3700 'span' => [ qw( 5 1 1 ) ], # unitprices?
3701 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3704 sub _condense_section {
3705 my ( $self, $format ) = ( shift, shift );
3707 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3708 qw( description_generator
3711 total_line_generator
3716 sub _condensed_generator_defaults {
3717 my ( $self, $format ) = ( shift, shift );
3718 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3727 sub _condensed_header_generator {
3728 my ( $self, $format ) = ( shift, shift );
3730 my ( $f, $prefix, $suffix, $separator, $column ) =
3731 _condensed_generator_defaults($format);
3733 if ($format eq 'latex') {
3734 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3735 $suffix = "\\\\\n\\hline";
3738 sub { my ($d,$a,$s,$w) = @_;
3739 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3741 } elsif ( $format eq 'html' ) {
3742 $prefix = '<th></th>';
3746 sub { my ($d,$a,$s,$w) = @_;
3747 return qq!<th align="$html_align{$a}">$d</th>!;
3755 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3757 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3760 $prefix. join($separator, @result). $suffix;
3765 sub _condensed_description_generator {
3766 my ( $self, $format ) = ( shift, shift );
3768 my ( $f, $prefix, $suffix, $separator, $column ) =
3769 _condensed_generator_defaults($format);
3771 my $money_char = '$';
3772 if ($format eq 'latex') {
3773 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3775 $separator = " & \n";
3777 sub { my ($d,$a,$s,$w) = @_;
3778 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3780 $money_char = '\\dollar';
3781 }elsif ( $format eq 'html' ) {
3782 $prefix = '"><td align="center"></td>';
3786 sub { my ($d,$a,$s,$w) = @_;
3787 return qq!<td align="$html_align{$a}">$d</td>!;
3789 #$money_char = $conf->config('money_char') || '$';
3790 $money_char = ''; # this is madness
3798 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3800 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3802 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3803 map { $f->{$_}->[$i] } qw(align span width)
3807 $prefix. join( $separator, @result ). $suffix;
3812 sub _condensed_total_generator {
3813 my ( $self, $format ) = ( shift, shift );
3815 my ( $f, $prefix, $suffix, $separator, $column ) =
3816 _condensed_generator_defaults($format);
3819 if ($format eq 'latex') {
3822 $separator = " & \n";
3824 sub { my ($d,$a,$s,$w) = @_;
3825 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3827 }elsif ( $format eq 'html' ) {
3831 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3833 sub { my ($d,$a,$s,$w) = @_;
3834 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3843 # my $r = &{$f->{fields}->[$i]}(@args);
3844 # $r .= ' Total' unless $i;
3846 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3848 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3849 map { $f->{$_}->[$i] } qw(align span width)
3853 $prefix. join( $separator, @result ). $suffix;
3858 =item total_line_generator FORMAT
3860 Returns a coderef used for generation of invoice total line items for this
3861 usage_class. FORMAT is either html or latex
3865 # should not be used: will have issues with hash element names (description vs
3866 # total_item and amount vs total_amount -- another array of functions?
3868 sub _condensed_total_line_generator {
3869 my ( $self, $format ) = ( shift, shift );
3871 my ( $f, $prefix, $suffix, $separator, $column ) =
3872 _condensed_generator_defaults($format);
3875 if ($format eq 'latex') {
3878 $separator = " & \n";
3880 sub { my ($d,$a,$s,$w) = @_;
3881 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3883 }elsif ( $format eq 'html' ) {
3887 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3889 sub { my ($d,$a,$s,$w) = @_;
3890 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3899 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3901 &{$column}( &{$f->{fields}->[$i]}(@args),
3902 map { $f->{$_}->[$i] } qw(align span width)
3906 $prefix. join( $separator, @result ). $suffix;
3911 #sub _items_extra_usage_sections {
3913 # my $escape = shift;
3915 # my %sections = ();
3917 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3918 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3920 # next unless $cust_bill_pkg->pkgnum > 0;
3922 # foreach my $section ( keys %usage_class ) {
3924 # my $usage = $cust_bill_pkg->usage($section);
3926 # next unless $usage && $usage > 0;
3928 # $sections{$section} ||= 0;
3929 # $sections{$section} += $usage;
3935 # map { { 'description' => &{$escape}($_),
3936 # 'subtotal' => $sections{$_},
3937 # 'summarized' => '',
3938 # 'tax_section' => '',
3941 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3945 sub _items_extra_usage_sections {
3954 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
3956 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3957 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3958 next unless $cust_bill_pkg->pkgnum > 0;
3960 foreach my $classnum ( keys %usage_class ) {
3961 my $section = $usage_class{$classnum}->classname;
3962 $classnums{$section} = $classnum;
3964 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3965 my $amount = $detail->amount;
3966 next unless $amount && $amount > 0;
3968 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3969 $sections{$section}{amount} += $amount; #subtotal
3970 $sections{$section}{calls}++;
3971 $sections{$section}{duration} += $detail->duration;
3973 my $desc = $detail->regionname;
3974 my $description = $desc;
3975 $description = substr($desc, 0, $maxlength). '...'
3976 if $format eq 'latex' && length($desc) > $maxlength;
3978 $lines{$section}{$desc} ||= {
3979 description => &{$escape}($description),
3980 #pkgpart => $part_pkg->pkgpart,
3981 pkgnum => $cust_bill_pkg->pkgnum,
3986 #unit_amount => $cust_bill_pkg->unitrecur,
3987 quantity => $cust_bill_pkg->quantity,
3988 product_code => 'N/A',
3989 ext_description => [],
3992 $lines{$section}{$desc}{amount} += $amount;
3993 $lines{$section}{$desc}{calls}++;
3994 $lines{$section}{$desc}{duration} += $detail->duration;
4000 my %sectionmap = ();
4001 foreach (keys %sections) {
4002 my $usage_class = $usage_class{$classnums{$_}};
4003 $sectionmap{$_} = { 'description' => &{$escape}($_),
4004 'amount' => $sections{$_}{amount}, #subtotal
4005 'calls' => $sections{$_}{calls},
4006 'duration' => $sections{$_}{duration},
4008 'tax_section' => '',
4009 'sort_weight' => $usage_class->weight,
4010 ( $usage_class->format
4011 ? ( map { $_ => $usage_class->$_($format) }
4012 qw( description_generator header_generator total_generator total_line_generator )
4019 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4023 foreach my $section ( keys %lines ) {
4024 foreach my $line ( keys %{$lines{$section}} ) {
4025 my $l = $lines{$section}{$line};
4026 $l->{section} = $sectionmap{$section};
4027 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4028 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4033 return(\@sections, \@lines);
4039 my $end = $self->_date;
4041 # start at date of previous invoice + 1 second or 0 if no previous invoice
4042 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4043 $start = 0 if !$start;
4046 my $cust_main = $self->cust_main;
4047 my @pkgs = $cust_main->all_pkgs;
4048 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4051 foreach my $pkg ( @pkgs ) {
4052 my @h_cust_svc = $pkg->h_cust_svc($end);
4053 foreach my $h_cust_svc ( @h_cust_svc ) {
4054 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4055 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4057 my $inserted = $h_cust_svc->date_inserted;
4058 my $deleted = $h_cust_svc->date_deleted;
4059 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4061 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4063 # DID either activated or ported in; cannot be both for same DID simultaneously
4064 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4065 && (!$phone_inserted->lnp_status
4066 || $phone_inserted->lnp_status eq ''
4067 || $phone_inserted->lnp_status eq 'native')) {
4070 else { # this one not so clean, should probably move to (h_)svc_phone
4071 my $phone_portedin = qsearchs( 'h_svc_phone',
4072 { 'svcnum' => $h_cust_svc->svcnum,
4073 'lnp_status' => 'portedin' },
4074 FS::h_svc_phone->sql_h_searchs($end),
4076 $num_portedin++ if $phone_portedin;
4079 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4080 if($deleted >= $start && $deleted <= $end && $phone_deleted
4081 && (!$phone_deleted->lnp_status
4082 || $phone_deleted->lnp_status ne 'portingout')) {
4085 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4086 && $phone_deleted->lnp_status
4087 && $phone_deleted->lnp_status eq 'portingout') {
4091 # increment usage minutes
4092 if ( $phone_inserted ) {
4093 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4094 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4097 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4100 # don't look at this service again
4101 push @seen, $h_cust_svc->svcnum;
4105 $minutes = sprintf("%d", $minutes);
4106 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4107 . "$num_deactivated Ported-Out: $num_portedout ",
4108 "Total Minutes: $minutes");
4111 sub _items_svc_phone_sections {
4120 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4122 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4123 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4125 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4126 next unless $cust_bill_pkg->pkgnum > 0;
4128 my @header = $cust_bill_pkg->details_header;
4129 next unless scalar(@header);
4131 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4133 my $phonenum = $detail->phonenum;
4134 next unless $phonenum;
4136 my $amount = $detail->amount;
4137 next unless $amount && $amount > 0;
4139 $sections{$phonenum} ||= { 'amount' => 0,
4142 'sort_weight' => -1,
4143 'phonenum' => $phonenum,
4145 $sections{$phonenum}{amount} += $amount; #subtotal
4146 $sections{$phonenum}{calls}++;
4147 $sections{$phonenum}{duration} += $detail->duration;
4149 my $desc = $detail->regionname;
4150 my $description = $desc;
4151 $description = substr($desc, 0, $maxlength). '...'
4152 if $format eq 'latex' && length($desc) > $maxlength;
4154 $lines{$phonenum}{$desc} ||= {
4155 description => &{$escape}($description),
4156 #pkgpart => $part_pkg->pkgpart,
4164 product_code => 'N/A',
4165 ext_description => [],
4168 $lines{$phonenum}{$desc}{amount} += $amount;
4169 $lines{$phonenum}{$desc}{calls}++;
4170 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4172 my $line = $usage_class{$detail->classnum}->classname;
4173 $sections{"$phonenum $line"} ||=
4177 'sort_weight' => $usage_class{$detail->classnum}->weight,
4178 'phonenum' => $phonenum,
4179 'header' => [ @header ],
4181 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4182 $sections{"$phonenum $line"}{calls}++;
4183 $sections{"$phonenum $line"}{duration} += $detail->duration;
4185 $lines{"$phonenum $line"}{$desc} ||= {
4186 description => &{$escape}($description),
4187 #pkgpart => $part_pkg->pkgpart,
4195 product_code => 'N/A',
4196 ext_description => [],
4199 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4200 $lines{"$phonenum $line"}{$desc}{calls}++;
4201 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4202 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4203 $detail->formatted('format' => $format);
4208 my %sectionmap = ();
4209 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4210 foreach ( keys %sections ) {
4211 my @header = @{ $sections{$_}{header} || [] };
4213 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4214 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4215 my $usage_class = $summary ? $simple : $usage_simple;
4216 my $ending = $summary ? ' usage charges' : '';
4219 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4221 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4222 'amount' => $sections{$_}{amount}, #subtotal
4223 'calls' => $sections{$_}{calls},
4224 'duration' => $sections{$_}{duration},
4226 'tax_section' => '',
4227 'phonenum' => $sections{$_}{phonenum},
4228 'sort_weight' => $sections{$_}{sort_weight},
4229 'post_total' => $summary, #inspire pagebreak
4231 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4232 qw( description_generator
4235 total_line_generator
4242 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4243 $a->{sort_weight} <=> $b->{sort_weight}
4248 foreach my $section ( keys %lines ) {
4249 foreach my $line ( keys %{$lines{$section}} ) {
4250 my $l = $lines{$section}{$line};
4251 $l->{section} = $sectionmap{$section};
4252 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4253 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4258 if($conf->exists('phone_usage_class_summary')) {
4259 # this only works with Latex
4263 # after this, we'll have only two sections per DID:
4264 # Calls Summary and Calls Detail
4265 foreach my $section ( @sections ) {
4266 if($section->{'post_total'}) {
4267 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4268 $section->{'total_line_generator'} = sub { '' };
4269 $section->{'total_generator'} = sub { '' };
4270 $section->{'header_generator'} = sub { '' };
4271 $section->{'description_generator'} = '';
4272 push @newsections, $section;
4273 my %calls_detail = %$section;
4274 $calls_detail{'post_total'} = '';
4275 $calls_detail{'sort_weight'} = '';
4276 $calls_detail{'description_generator'} = sub { '' };
4277 $calls_detail{'header_generator'} = sub {
4278 return ' & Date/Time & Called Number & Duration & Price'
4279 if $format eq 'latex';
4282 $calls_detail{'description'} = 'Calls Detail: '
4283 . $section->{'phonenum'};
4284 push @newsections, \%calls_detail;
4288 # after this, each usage class is collapsed/summarized into a single
4289 # line under the Calls Summary section
4290 foreach my $newsection ( @newsections ) {
4291 if($newsection->{'post_total'}) { # this means Calls Summary
4292 foreach my $section ( @sections ) {
4293 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4294 && !$section->{'post_total'});
4295 my $newdesc = $section->{'description'};
4296 my $tn = $section->{'phonenum'};
4297 $newdesc =~ s/$tn//g;
4298 my $line = { ext_description => [],
4302 calls => $section->{'calls'},
4303 section => $newsection,
4304 duration => $section->{'duration'},
4305 description => $newdesc,
4306 amount => sprintf("%.2f",$section->{'amount'}),
4307 product_code => 'N/A',
4309 push @newlines, $line;
4314 # after this, Calls Details is populated with all CDRs
4315 foreach my $newsection ( @newsections ) {
4316 if(!$newsection->{'post_total'}) { # this means Calls Details
4317 foreach my $line ( @lines ) {
4318 next unless (scalar(@{$line->{'ext_description'}}) &&
4319 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4321 my @extdesc = @{$line->{'ext_description'}};
4323 foreach my $extdesc ( @extdesc ) {
4324 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4325 push @newextdesc, $extdesc;
4327 $line->{'ext_description'} = \@newextdesc;
4328 $line->{'section'} = $newsection;
4329 push @newlines, $line;
4334 return(\@newsections, \@newlines);
4337 return(\@sections, \@lines);
4344 #my @display = scalar(@_)
4346 # : qw( _items_previous _items_pkg );
4347 # #: qw( _items_pkg );
4348 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4349 my @display = qw( _items_previous _items_pkg );
4352 foreach my $display ( @display ) {
4353 push @b, $self->$display(@_);
4358 sub _items_previous {
4360 my $cust_main = $self->cust_main;
4361 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4363 foreach ( @pr_cust_bill ) {
4364 my $date = $conf->exists('invoice_show_prior_due_date')
4365 ? 'due '. $_->due_date2str($date_format)
4366 : time2str($date_format, $_->_date);
4368 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4369 #'pkgpart' => 'N/A',
4371 'amount' => sprintf("%.2f", $_->owed),
4377 # 'description' => 'Previous Balance',
4378 # #'pkgpart' => 'N/A',
4379 # 'pkgnum' => 'N/A',
4380 # 'amount' => sprintf("%10.2f", $pr_total ),
4381 # 'ext_description' => [ map {
4382 # "Invoice ". $_->invnum.
4383 # " (". time2str("%x",$_->_date). ") ".
4384 # sprintf("%10.2f", $_->owed)
4385 # } @pr_cust_bill ],
4394 warn "$me _items_pkg searching for all package line items\n"
4397 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4399 warn "$me _items_pkg filtering line items\n"
4401 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4403 if ($options{section} && $options{section}->{condensed}) {
4405 warn "$me _items_pkg condensing section\n"
4409 local $Storable::canonical = 1;
4410 foreach ( @items ) {
4412 delete $item->{ref};
4413 delete $item->{ext_description};
4414 my $key = freeze($item);
4415 $itemshash{$key} ||= 0;
4416 $itemshash{$key} ++; # += $item->{quantity};
4418 @items = sort { $a->{description} cmp $b->{description} }
4419 map { my $i = thaw($_);
4420 $i->{quantity} = $itemshash{$_};
4422 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4428 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4435 return 0 unless $a->itemdesc cmp $b->itemdesc;
4436 return -1 if $b->itemdesc eq 'Tax';
4437 return 1 if $a->itemdesc eq 'Tax';
4438 return -1 if $b->itemdesc eq 'Other surcharges';
4439 return 1 if $a->itemdesc eq 'Other surcharges';
4440 $a->itemdesc cmp $b->itemdesc;
4445 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4446 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4449 sub _items_cust_bill_pkg {
4451 my $cust_bill_pkgs = shift;
4454 my $format = $opt{format} || '';
4455 my $escape_function = $opt{escape_function} || sub { shift };
4456 my $format_function = $opt{format_function} || '';
4457 my $unsquelched = $opt{unsquelched} || '';
4458 my $section = $opt{section}->{description} if $opt{section};
4459 my $summary_page = $opt{summary_page} || '';
4460 my $multilocation = $opt{multilocation} || '';
4461 my $multisection = $opt{multisection} || '';
4462 my $discount_show_always = 0;
4464 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4467 my ($s, $r, $u) = ( undef, undef, undef );
4468 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4471 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4472 if ( $_ && !$cust_bill_pkg->hidden ) {
4473 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4474 $_->{amount} =~ s/^\-0\.00$/0.00/;
4475 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4477 if $_->{amount} != 0
4478 || $discount_show_always
4479 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4480 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4486 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4487 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4490 foreach my $display ( grep { defined($section)
4491 ? $_->section eq $section
4494 #grep { !$_->summary || !$summary_page } # bunk!
4495 grep { !$_->summary || $multisection }
4496 $cust_bill_pkg->cust_bill_pkg_display
4500 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4501 $display->billpkgdisplaynum. "\n"
4504 my $type = $display->type;
4506 my $desc = $cust_bill_pkg->desc;
4507 $desc = substr($desc, 0, $maxlength). '...'
4508 if $format eq 'latex' && length($desc) > $maxlength;
4510 my %details_opt = ( 'format' => $format,
4511 'escape_function' => $escape_function,
4512 'format_function' => $format_function,
4515 if ( $cust_bill_pkg->pkgnum > 0 ) {
4517 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4520 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4522 if ( (!$type || $type eq 'S')
4523 && ( $cust_bill_pkg->setup != 0
4524 || $cust_bill_pkg->setup_show_zero
4529 warn "$me _items_cust_bill_pkg adding setup\n"
4532 my $description = $desc;
4533 $description .= ' Setup'
4534 if $cust_bill_pkg->recur != 0
4535 || $discount_show_always
4536 || $cust_bill_pkg->recur_show_zero;
4539 unless ( $cust_pkg->part_pkg->hide_svc_detail
4540 || $cust_bill_pkg->hidden )
4543 push @d, map &{$escape_function}($_),
4544 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4545 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4547 if ( $multilocation ) {
4548 my $loc = $cust_pkg->location_label;
4549 $loc = substr($loc, 0, $maxlength). '...'
4550 if $format eq 'latex' && length($loc) > $maxlength;
4551 push @d, &{$escape_function}($loc);
4556 push @d, $cust_bill_pkg->details(%details_opt)
4557 if $cust_bill_pkg->recur == 0;
4559 if ( $cust_bill_pkg->hidden ) {
4560 $s->{amount} += $cust_bill_pkg->setup;
4561 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4562 push @{ $s->{ext_description} }, @d;
4566 description => $description,
4567 #pkgpart => $part_pkg->pkgpart,
4568 pkgnum => $cust_bill_pkg->pkgnum,
4569 amount => $cust_bill_pkg->setup,
4570 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4571 unit_amount => $cust_bill_pkg->unitsetup,
4572 quantity => $cust_bill_pkg->quantity,
4573 ext_description => \@d,
4579 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4581 $cust_bill_pkg->recur != 0
4582 || $cust_bill_pkg->setup == 0
4583 || $discount_show_always
4584 || $cust_bill_pkg->recur_show_zero
4589 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4592 my $is_summary = $display->summary;
4593 my $description = ($is_summary && $type && $type eq 'U')
4594 ? "Usage charges" : $desc;
4596 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4597 " - ". time2str($date_format, $cust_bill_pkg->edate).
4599 unless $conf->exists('disable_line_item_date_ranges')
4600 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4604 #at least until cust_bill_pkg has "past" ranges in addition to
4605 #the "future" sdate/edate ones... see #3032
4606 my @dates = ( $self->_date );
4607 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4608 push @dates, $prev->sdate if $prev;
4609 push @dates, undef if !$prev;
4611 unless ( $cust_pkg->part_pkg->hide_svc_detail
4612 || $cust_bill_pkg->itemdesc
4613 || $cust_bill_pkg->hidden
4614 || $is_summary && $type && $type eq 'U' )
4617 warn "$me _items_cust_bill_pkg adding service details\n"
4620 push @d, map &{$escape_function}($_),
4621 $cust_pkg->h_labels_short(@dates, 'I')
4622 #$cust_bill_pkg->edate,
4623 #$cust_bill_pkg->sdate)
4624 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4626 warn "$me _items_cust_bill_pkg done adding service details\n"
4629 if ( $multilocation ) {
4630 my $loc = $cust_pkg->location_label;
4631 $loc = substr($loc, 0, $maxlength). '...'
4632 if $format eq 'latex' && length($loc) > $maxlength;
4633 push @d, &{$escape_function}($loc);
4638 unless ( $is_summary ) {
4639 warn "$me _items_cust_bill_pkg adding details\n"
4642 #instead of omitting details entirely in this case (unwanted side
4643 # effects), just omit CDRs
4644 $details_opt{'format_function'} = sub { () }
4645 if $type && $type eq 'R';
4647 push @d, $cust_bill_pkg->details(%details_opt);
4650 warn "$me _items_cust_bill_pkg calculating amount\n"
4655 $amount = $cust_bill_pkg->recur;
4656 } elsif ($type eq 'R') {
4657 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4658 } elsif ($type eq 'U') {
4659 $amount = $cust_bill_pkg->usage;
4662 if ( !$type || $type eq 'R' ) {
4664 warn "$me _items_cust_bill_pkg adding recur\n"
4667 if ( $cust_bill_pkg->hidden ) {
4668 $r->{amount} += $amount;
4669 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4670 push @{ $r->{ext_description} }, @d;
4673 description => $description,
4674 #pkgpart => $part_pkg->pkgpart,
4675 pkgnum => $cust_bill_pkg->pkgnum,
4677 recur_show_zero => $cust_bill_pkg->recur_show_zero,
4678 unit_amount => $cust_bill_pkg->unitrecur,
4679 quantity => $cust_bill_pkg->quantity,
4680 ext_description => \@d,
4684 } else { # $type eq 'U'
4686 warn "$me _items_cust_bill_pkg adding usage\n"
4689 if ( $cust_bill_pkg->hidden ) {
4690 $u->{amount} += $amount;
4691 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4692 push @{ $u->{ext_description} }, @d;
4695 description => $description,
4696 #pkgpart => $part_pkg->pkgpart,
4697 pkgnum => $cust_bill_pkg->pkgnum,
4699 recur_show_zero => $cust_bill_pkg->recur_show_zero,
4700 unit_amount => $cust_bill_pkg->unitrecur,
4701 quantity => $cust_bill_pkg->quantity,
4702 ext_description => \@d,
4708 } # recurring or usage with recurring charge
4710 } else { #pkgnum tax or one-shot line item (??)
4712 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4715 if ( $cust_bill_pkg->setup != 0 ) {
4717 'description' => $desc,
4718 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4721 if ( $cust_bill_pkg->recur != 0 ) {
4723 'description' => "$desc (".
4724 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4725 time2str($date_format, $cust_bill_pkg->edate). ')',
4726 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4734 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4735 && $conf->exists('discount-show-always'));
4739 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4741 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4742 $_->{amount} =~ s/^\-0\.00$/0.00/;
4743 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4745 if $_->{amount} != 0
4746 || $discount_show_always
4747 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4748 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4752 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4759 sub _items_credits {
4760 my( $self, %opt ) = @_;
4761 my $trim_len = $opt{'trim_len'} || 60;
4765 foreach ( $self->cust_credited ) {
4767 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4769 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4770 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4771 $reason = " ($reason) " if $reason;
4774 #'description' => 'Credit ref\#'. $_->crednum.
4775 # " (". time2str("%x",$_->cust_credit->_date) .")".
4777 'description' => 'Credit applied '.
4778 time2str($date_format,$_->cust_credit->_date). $reason,
4779 'amount' => sprintf("%.2f",$_->amount),
4787 sub _items_payments {
4791 #get & print payments
4792 foreach ( $self->cust_bill_pay ) {
4794 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4797 'description' => "Payment received ".
4798 time2str($date_format,$_->cust_pay->_date ),
4799 'amount' => sprintf("%.2f", $_->amount )
4807 =item call_details [ OPTION => VALUE ... ]
4809 Returns an array of CSV strings representing the call details for this invoice
4810 The only option available is the boolean prepend_billed_number
4815 my ($self, %opt) = @_;
4817 my $format_function = sub { shift };
4819 if ($opt{prepend_billed_number}) {
4820 $format_function = sub {
4824 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4829 my @details = map { $_->details( 'format_function' => $format_function,
4830 'escape_function' => sub{ return() },
4834 $self->cust_bill_pkg;
4835 my $header = $details[0];
4836 ( $header, grep { $_ ne $header } @details );
4846 =item process_reprint
4850 sub process_reprint {
4851 process_re_X('print', @_);
4854 =item process_reemail
4858 sub process_reemail {
4859 process_re_X('email', @_);
4867 process_re_X('fax', @_);
4875 process_re_X('ftp', @_);
4882 sub process_respool {
4883 process_re_X('spool', @_);
4886 use Storable qw(thaw);
4890 my( $method, $job ) = ( shift, shift );
4891 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4893 my $param = thaw(decode_base64(shift));
4894 warn Dumper($param) if $DEBUG;
4905 my($method, $job, %param ) = @_;
4907 warn "re_X $method for job $job with param:\n".
4908 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4911 #some false laziness w/search/cust_bill.html
4913 my $orderby = 'ORDER BY cust_bill._date';
4915 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4917 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4919 my @cust_bill = qsearch( {
4920 #'select' => "cust_bill.*",
4921 'table' => 'cust_bill',
4922 'addl_from' => $addl_from,
4924 'extra_sql' => $extra_sql,
4925 'order_by' => $orderby,
4929 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4931 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4934 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4935 foreach my $cust_bill ( @cust_bill ) {
4936 $cust_bill->$method();
4938 if ( $job ) { #progressbar foo
4940 if ( time - $min_sec > $last ) {
4941 my $error = $job->update_statustext(
4942 int( 100 * $num / scalar(@cust_bill) )
4944 die $error if $error;
4955 =head1 CLASS METHODS
4961 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4966 my ($class, $start, $end) = @_;
4968 $class->paid_sql($start, $end). ' - '.
4969 $class->credited_sql($start, $end);
4974 Returns an SQL fragment to retreive the net amount (charged minus credited).
4979 my ($class, $start, $end) = @_;
4980 'charged - '. $class->credited_sql($start, $end);
4985 Returns an SQL fragment to retreive the amount paid against this invoice.
4990 my ($class, $start, $end) = @_;
4991 $start &&= "AND cust_bill_pay._date <= $start";
4992 $end &&= "AND cust_bill_pay._date > $end";
4993 $start = '' unless defined($start);
4994 $end = '' unless defined($end);
4995 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4996 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5001 Returns an SQL fragment to retreive the amount credited against this invoice.
5006 my ($class, $start, $end) = @_;
5007 $start &&= "AND cust_credit_bill._date <= $start";
5008 $end &&= "AND cust_credit_bill._date > $end";
5009 $start = '' unless defined($start);
5010 $end = '' unless defined($end);
5011 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5012 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5017 Returns an SQL fragment to retrieve the due date of an invoice.
5018 Currently only supported on PostgreSQL.
5026 cust_bill.invoice_terms,
5027 cust_main.invoice_terms,
5028 \''.($conf->config('invoice_default_terms') || '').'\'
5029 ), E\'Net (\\\\d+)\'
5031 ) * 86400 + cust_bill._date'
5034 =item search_sql_where HASHREF
5036 Class method which returns an SQL WHERE fragment to search for parameters
5037 specified in HASHREF. Valid parameters are
5043 List reference of start date, end date, as UNIX timestamps.
5053 List reference of charged limits (exclusive).
5057 List reference of charged limits (exclusive).
5061 flag, return open invoices only
5065 flag, return net invoices only
5069 =item newest_percust
5073 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5077 sub search_sql_where {
5078 my($class, $param) = @_;
5080 warn "$me search_sql_where called with params: \n".
5081 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5087 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5088 push @search, "cust_main.agentnum = $1";
5092 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5093 push @search, "cust_bill.custnum = $1";
5097 if ( $param->{_date} ) {
5098 my($beginning, $ending) = @{$param->{_date}};
5100 push @search, "cust_bill._date >= $beginning",
5101 "cust_bill._date < $ending";
5105 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5106 push @search, "cust_bill.invnum >= $1";
5108 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5109 push @search, "cust_bill.invnum <= $1";
5113 if ( $param->{charged} ) {
5114 my @charged = ref($param->{charged})
5115 ? @{ $param->{charged} }
5116 : ($param->{charged});
5118 push @search, map { s/^charged/cust_bill.charged/; $_; }
5122 my $owed_sql = FS::cust_bill->owed_sql;
5125 if ( $param->{owed} ) {
5126 my @owed = ref($param->{owed})
5127 ? @{ $param->{owed} }
5129 push @search, map { s/^owed/$owed_sql/; $_; }
5134 push @search, "0 != $owed_sql"
5135 if $param->{'open'};
5136 push @search, '0 != '. FS::cust_bill->net_sql
5140 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5141 if $param->{'days'};
5144 if ( $param->{'newest_percust'} ) {
5146 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5147 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5149 my @newest_where = map { my $x = $_;
5150 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5153 grep ! /^cust_main./, @search;
5154 my $newest_where = scalar(@newest_where)
5155 ? ' AND '. join(' AND ', @newest_where)
5159 push @search, "cust_bill._date = (
5160 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5161 WHERE newest_cust_bill.custnum = cust_bill.custnum
5167 #agent virtualization
5168 my $curuser = $FS::CurrentUser::CurrentUser;
5169 if ( $curuser->username eq 'fs_queue'
5170 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5172 my $newuser = qsearchs('access_user', {
5173 'username' => $username,
5177 $curuser = $newuser;
5179 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5182 push @search, $curuser->agentnums_sql;
5184 join(' AND ', @search );
5196 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5197 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base