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( unsquealch_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( unsquealch_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 ],
2864 foreach my $section (@sections, @$late_sections) {
2866 warn "$me adding section \n". Dumper($section)
2869 # begin some normalization
2870 $section->{'subtotal'} = $section->{'amount'}
2872 && !exists($section->{subtotal})
2873 && exists($section->{amount});
2875 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2876 if ( $invoice_data{finance_section} &&
2877 $section->{'description'} eq $invoice_data{finance_section} );
2879 $section->{'subtotal'} = $other_money_char.
2880 sprintf('%.2f', $section->{'subtotal'})
2883 # continue some normalization
2884 $section->{'amount'} = $section->{'subtotal'}
2888 if ( $section->{'description'} ) {
2889 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2894 warn "$me setting options\n"
2897 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2899 $options{'section'} = $section if $multisection;
2900 $options{'format'} = $format;
2901 $options{'escape_function'} = $escape_function;
2902 $options{'format_function'} = sub { () } unless $unsquelched;
2903 $options{'unsquelched'} = $unsquelched;
2904 $options{'summary_page'} = $summarypage;
2905 $options{'skip_usage'} =
2906 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2907 $options{'multilocation'} = $multilocation;
2908 $options{'multisection'} = $multisection;
2910 warn "$me searching for line items\n"
2913 foreach my $line_item ( $self->_items_pkg(%options) ) {
2915 warn "$me adding line item $line_item\n"
2919 ext_description => [],
2921 $detail->{'ref'} = $line_item->{'pkgnum'};
2922 $detail->{'quantity'} = $line_item->{'quantity'};
2923 $detail->{'section'} = $section;
2924 $detail->{'description'} = &$escape_function($line_item->{'description'});
2925 if ( exists $line_item->{'ext_description'} ) {
2926 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2928 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2929 $line_item->{'amount'};
2930 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2931 $line_item->{'unit_amount'};
2932 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2934 push @detail_items, $detail;
2935 push @buf, ( [ $detail->{'description'},
2936 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2938 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2942 if ( $section->{'description'} ) {
2943 push @buf, ( ['','-----------'],
2944 [ $section->{'description'}. ' sub-total',
2945 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2954 $invoice_data{current_less_finance} =
2955 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2957 if ( $multisection && !$conf->exists('disable_previous_balance')
2958 || $conf->exists('previous_balance-summary_only') )
2960 unshift @sections, $previous_section if $pr_total;
2963 warn "$me adding taxes\n"
2966 foreach my $tax ( $self->_items_tax ) {
2968 $taxtotal += $tax->{'amount'};
2970 my $description = &$escape_function( $tax->{'description'} );
2971 my $amount = sprintf( '%.2f', $tax->{'amount'} );
2973 if ( $multisection ) {
2975 my $money = $old_latex ? '' : $money_char;
2976 push @detail_items, {
2977 ext_description => [],
2980 description => $description,
2981 amount => $money. $amount,
2983 section => $tax_section,
2988 push @total_items, {
2989 'total_item' => $description,
2990 'total_amount' => $other_money_char. $amount,
2995 push @buf,[ $description,
2996 $money_char. $amount,
3003 $total->{'total_item'} = 'Sub-total';
3004 $total->{'total_amount'} =
3005 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3007 if ( $multisection ) {
3008 $tax_section->{'subtotal'} = $other_money_char.
3009 sprintf('%.2f', $taxtotal);
3010 $tax_section->{'pretotal'} = 'New charges sub-total '.
3011 $total->{'total_amount'};
3012 push @sections, $tax_section if $taxtotal;
3014 unshift @total_items, $total;
3017 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3019 push @buf,['','-----------'];
3020 push @buf,[( $conf->exists('disable_previous_balance')
3022 : 'Total New Charges'
3024 $money_char. sprintf("%10.2f",$self->charged) ];
3030 $item = $conf->config('previous_balance-exclude_from_total')
3031 || 'Total New Charges'
3032 if $conf->exists('previous_balance-exclude_from_total');
3033 my $amount = $self->charged +
3034 ( $conf->exists('disable_previous_balance') ||
3035 $conf->exists('previous_balance-exclude_from_total')
3039 $total->{'total_item'} = &$embolden_function($item);
3040 $total->{'total_amount'} =
3041 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3042 if ( $multisection ) {
3043 if ( $adjust_section->{'sort_weight'} ) {
3044 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3045 sprintf("%.2f", ($self->billing_balance || 0) );
3047 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3048 sprintf('%.2f', $self->charged );
3051 push @total_items, $total;
3053 push @buf,['','-----------'];
3056 sprintf( '%10.2f', $amount )
3061 unless ( $conf->exists('disable_previous_balance') ) {
3062 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3065 my $credittotal = 0;
3066 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3069 $total->{'total_item'} = &$escape_function($credit->{'description'});
3070 $credittotal += $credit->{'amount'};
3071 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3072 $adjusttotal += $credit->{'amount'};
3073 if ( $multisection ) {
3074 my $money = $old_latex ? '' : $money_char;
3075 push @detail_items, {
3076 ext_description => [],
3079 description => &$escape_function($credit->{'description'}),
3080 amount => $money. $credit->{'amount'},
3082 section => $adjust_section,
3085 push @total_items, $total;
3089 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3092 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3093 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3097 my $paymenttotal = 0;
3098 foreach my $payment ( $self->_items_payments ) {
3100 $total->{'total_item'} = &$escape_function($payment->{'description'});
3101 $paymenttotal += $payment->{'amount'};
3102 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3103 $adjusttotal += $payment->{'amount'};
3104 if ( $multisection ) {
3105 my $money = $old_latex ? '' : $money_char;
3106 push @detail_items, {
3107 ext_description => [],
3110 description => &$escape_function($payment->{'description'}),
3111 amount => $money. $payment->{'amount'},
3113 section => $adjust_section,
3116 push @total_items, $total;
3118 push @buf, [ $payment->{'description'},
3119 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3122 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3124 if ( $multisection ) {
3125 $adjust_section->{'subtotal'} = $other_money_char.
3126 sprintf('%.2f', $adjusttotal);
3127 push @sections, $adjust_section
3128 unless $adjust_section->{sort_weight};
3133 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3134 $total->{'total_amount'} =
3135 &$embolden_function(
3136 $other_money_char. sprintf('%.2f', $summarypage
3138 $self->billing_balance
3139 : $self->owed + $pr_total
3142 if ( $multisection && !$adjust_section->{sort_weight} ) {
3143 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3144 $total->{'total_amount'};
3146 push @total_items, $total;
3148 push @buf,['','-----------'];
3149 push @buf,[$self->balance_due_msg, $money_char.
3150 sprintf("%10.2f", $balance_due ) ];
3153 if ( $conf->exists('previous_balance-show_credit')
3154 and $cust_main->balance < 0 ) {
3155 my $credit_total = {
3156 'total_item' => &$embolden_function($self->credit_balance_msg),
3157 'total_amount' => &$embolden_function(
3158 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3161 if ( $multisection ) {
3162 $adjust_section->{'posttotal'} .= $newline_token .
3163 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3166 push @total_items, $credit_total;
3168 push @buf,['','-----------'];
3169 push @buf,[$self->credit_balance_msg, $money_char.
3170 sprintf("%10.2f", -$cust_main->balance ) ];
3174 if ( $multisection ) {
3175 if ($conf->exists('svc_phone_sections')) {
3177 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3178 $total->{'total_amount'} =
3179 &$embolden_function(
3180 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3182 my $last_section = pop @sections;
3183 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3184 $total->{'total_amount'};
3185 push @sections, $last_section;
3187 push @sections, @$late_sections
3191 my @includelist = ();
3192 push @includelist, 'summary' if $summarypage;
3193 foreach my $include ( @includelist ) {
3195 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3198 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3200 @inc_src = $conf->config($inc_file, $agentnum);
3204 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3206 my $convert_map = $convert_maps{$format}{$include};
3208 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3209 s/--\@\]/$delimiters{$format}[1]/g;
3212 &$convert_map( $conf->config($inc_file, $agentnum) );
3216 my $inc_tt = new Text::Template (
3218 SOURCE => [ map "$_\n", @inc_src ],
3219 DELIMITERS => $delimiters{$format},
3220 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3222 unless ( $inc_tt->compile() ) {
3223 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3224 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3228 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3230 $invoice_data{$include} =~ s/\n+$//
3231 if ($format eq 'latex');
3236 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3237 /invoice_lines\((\d*)\)/;
3238 $invoice_lines += $1 || scalar(@buf);
3241 die "no invoice_lines() functions in template?"
3242 if ( $format eq 'template' && !$wasfunc );
3244 if ($format eq 'template') {
3246 if ( $invoice_lines ) {
3247 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3248 $invoice_data{'total_pages'}++
3249 if scalar(@buf) % $invoice_lines;
3252 #setup subroutine for the template
3253 sub FS::cust_bill::_template::invoice_lines {
3254 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3256 scalar(@FS::cust_bill::_template::buf)
3257 ? shift @FS::cust_bill::_template::buf
3266 push @collect, split("\n",
3267 $text_template->fill_in( HASH => \%invoice_data,
3268 PACKAGE => 'FS::cust_bill::_template'
3271 $FS::cust_bill::_template::page++;
3273 map "$_\n", @collect;
3275 warn "filling in template for invoice ". $self->invnum. "\n"
3277 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3280 $text_template->fill_in(HASH => \%invoice_data);
3284 # helper routine for generating date ranges
3285 sub _prior_month30s {
3288 [ 1, 2592000 ], # 0-30 days ago
3289 [ 2592000, 5184000 ], # 30-60 days ago
3290 [ 5184000, 7776000 ], # 60-90 days ago
3291 [ 7776000, 0 ], # 90+ days ago
3294 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3295 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3300 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3302 Returns an postscript invoice, as a scalar.
3304 Options can be passed as a hashref (recommended) or as a list of time, template
3305 and then any key/value pairs for any other options.
3307 I<time> an optional value used to control the printing of overdue messages. The
3308 default is now. It isn't the date of the invoice; that's the `_date' field.
3309 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3310 L<Time::Local> and L<Date::Parse> for conversion functions.
3312 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3319 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3320 my $ps = generate_ps($file);
3322 unlink($barcodefile) if $barcodefile;
3327 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3329 Returns an PDF invoice, as a scalar.
3331 Options can be passed as a hashref (recommended) or as a list of time, template
3332 and then any key/value pairs for any other options.
3334 I<time> an optional value used to control the printing of overdue messages. The
3335 default is now. It isn't the date of the invoice; that's the `_date' field.
3336 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3337 L<Time::Local> and L<Date::Parse> for conversion functions.
3339 I<template>, if specified, is the name of a suffix for alternate invoices.
3341 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3348 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3349 my $pdf = generate_pdf($file);
3351 unlink($barcodefile) if $barcodefile;
3356 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3358 Returns an HTML invoice, as a scalar.
3360 I<time> an optional value used to control the printing of overdue messages. The
3361 default is now. It isn't the date of the invoice; that's the `_date' field.
3362 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3363 L<Time::Local> and L<Date::Parse> for conversion functions.
3365 I<template>, if specified, is the name of a suffix for alternate invoices.
3367 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3369 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3370 when emailing the invoice as part of a multipart/related MIME email.
3378 %params = %{ shift() };
3380 $params{'time'} = shift;
3381 $params{'template'} = shift;
3382 $params{'cid'} = shift;
3385 $params{'format'} = 'html';
3387 $self->print_generic( %params );
3390 # quick subroutine for print_latex
3392 # There are ten characters that LaTeX treats as special characters, which
3393 # means that they do not simply typeset themselves:
3394 # # $ % & ~ _ ^ \ { }
3396 # TeX ignores blanks following an escaped character; if you want a blank (as
3397 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3401 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3402 $value =~ s/([<>])/\$$1\$/g;
3408 encode_entities($value);
3412 sub _html_escape_nbsp {
3413 my $value = _html_escape(shift);
3414 $value =~ s/ +/ /g;
3418 #utility methods for print_*
3420 sub _translate_old_latex_format {
3421 warn "_translate_old_latex_format called\n"
3428 if ( $line =~ /^%%Detail\s*$/ ) {
3430 push @template, q![@--!,
3431 q! foreach my $_tr_line (@detail_items) {!,
3432 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3433 q! $_tr_line->{'description'} .= !,
3434 q! "\\tabularnewline\n~~".!,
3435 q! join( "\\tabularnewline\n~~",!,
3436 q! @{$_tr_line->{'ext_description'}}!,
3440 while ( ( my $line_item_line = shift )
3441 !~ /^%%EndDetail\s*$/ ) {
3442 $line_item_line =~ s/'/\\'/g; # nice LTS
3443 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3444 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3445 push @template, " \$OUT .= '$line_item_line';";
3448 push @template, '}',
3451 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3453 push @template, '[@--',
3454 ' foreach my $_tr_line (@total_items) {';
3456 while ( ( my $total_item_line = shift )
3457 !~ /^%%EndTotalDetails\s*$/ ) {
3458 $total_item_line =~ s/'/\\'/g; # nice LTS
3459 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3460 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3461 push @template, " \$OUT .= '$total_item_line';";
3464 push @template, '}',
3468 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3469 push @template, $line;
3475 warn "$_\n" foreach @template;
3484 #check for an invoice-specific override
3485 return $self->invoice_terms if $self->invoice_terms;
3487 #check for a customer- specific override
3488 my $cust_main = $self->cust_main;
3489 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3491 #use configured default
3492 $conf->config('invoice_default_terms') || '';
3498 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3499 $duedate = $self->_date() + ( $1 * 86400 );
3506 $self->due_date ? time2str(shift, $self->due_date) : '';
3509 sub balance_due_msg {
3511 my $msg = 'Balance Due';
3512 return $msg unless $self->terms;
3513 if ( $self->due_date ) {
3514 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3515 } elsif ( $self->terms ) {
3516 $msg .= ' - '. $self->terms;
3521 sub balance_due_date {
3524 if ( $conf->exists('invoice_default_terms')
3525 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3526 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3531 sub credit_balance_msg { 'Credit Balance Remaining' }
3533 =item invnum_date_pretty
3535 Returns a string with the invoice number and date, for example:
3536 "Invoice #54 (3/20/2008)"
3540 sub invnum_date_pretty {
3542 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3547 Returns a string with the date, for example: "3/20/2008"
3553 time2str($date_format, $self->_date);
3556 use vars qw(%pkg_category_cache);
3557 sub _items_sections {
3560 my $summarypage = shift;
3562 my $extra_sections = shift;
3566 my %late_subtotal = ();
3569 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3572 my $usage = $cust_bill_pkg->usage;
3574 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3575 next if ( $display->summary && $summarypage );
3577 my $section = $display->section;
3578 my $type = $display->type;
3580 $not_tax{$section} = 1
3581 unless $cust_bill_pkg->pkgnum == 0;
3583 if ( $display->post_total && !$summarypage ) {
3584 if (! $type || $type eq 'S') {
3585 $late_subtotal{$section} += $cust_bill_pkg->setup
3586 if $cust_bill_pkg->setup != 0;
3590 $late_subtotal{$section} += $cust_bill_pkg->recur
3591 if $cust_bill_pkg->recur != 0;
3594 if ($type && $type eq 'R') {
3595 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3596 if $cust_bill_pkg->recur != 0;
3599 if ($type && $type eq 'U') {
3600 $late_subtotal{$section} += $usage
3601 unless scalar(@$extra_sections);
3606 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3608 if (! $type || $type eq 'S') {
3609 $subtotal{$section} += $cust_bill_pkg->setup
3610 if $cust_bill_pkg->setup != 0;
3614 $subtotal{$section} += $cust_bill_pkg->recur
3615 if $cust_bill_pkg->recur != 0;
3618 if ($type && $type eq 'R') {
3619 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3620 if $cust_bill_pkg->recur != 0;
3623 if ($type && $type eq 'U') {
3624 $subtotal{$section} += $usage
3625 unless scalar(@$extra_sections);
3634 %pkg_category_cache = ();
3636 push @$late, map { { 'description' => &{$escape}($_),
3637 'subtotal' => $late_subtotal{$_},
3639 'sort_weight' => ( _pkg_category($_)
3640 ? _pkg_category($_)->weight
3643 ((_pkg_category($_) && _pkg_category($_)->condense)
3644 ? $self->_condense_section($format)
3648 sort _sectionsort keys %late_subtotal;
3651 if ( $summarypage ) {
3652 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3653 map { $_->categoryname } qsearch('pkg_category', {});
3654 push @sections, '' if exists($subtotal{''});
3656 @sections = keys %subtotal;
3659 my @early = map { { 'description' => &{$escape}($_),
3660 'subtotal' => $subtotal{$_},
3661 'summarized' => $not_tax{$_} ? '' : 'Y',
3662 'tax_section' => $not_tax{$_} ? '' : 'Y',
3663 'sort_weight' => ( _pkg_category($_)
3664 ? _pkg_category($_)->weight
3667 ((_pkg_category($_) && _pkg_category($_)->condense)
3668 ? $self->_condense_section($format)
3673 push @early, @$extra_sections if $extra_sections;
3675 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3679 #helper subs for above
3682 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3686 my $categoryname = shift;
3687 $pkg_category_cache{$categoryname} ||=
3688 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3691 my %condensed_format = (
3692 'label' => [ qw( Description Qty Amount ) ],
3694 sub { shift->{description} },
3695 sub { shift->{quantity} },
3696 sub { my($href, %opt) = @_;
3697 ($opt{dollar} || ''). $href->{amount};
3700 'align' => [ qw( l r r ) ],
3701 'span' => [ qw( 5 1 1 ) ], # unitprices?
3702 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3705 sub _condense_section {
3706 my ( $self, $format ) = ( shift, shift );
3708 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3709 qw( description_generator
3712 total_line_generator
3717 sub _condensed_generator_defaults {
3718 my ( $self, $format ) = ( shift, shift );
3719 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3728 sub _condensed_header_generator {
3729 my ( $self, $format ) = ( shift, shift );
3731 my ( $f, $prefix, $suffix, $separator, $column ) =
3732 _condensed_generator_defaults($format);
3734 if ($format eq 'latex') {
3735 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3736 $suffix = "\\\\\n\\hline";
3739 sub { my ($d,$a,$s,$w) = @_;
3740 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3742 } elsif ( $format eq 'html' ) {
3743 $prefix = '<th></th>';
3747 sub { my ($d,$a,$s,$w) = @_;
3748 return qq!<th align="$html_align{$a}">$d</th>!;
3756 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3758 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3761 $prefix. join($separator, @result). $suffix;
3766 sub _condensed_description_generator {
3767 my ( $self, $format ) = ( shift, shift );
3769 my ( $f, $prefix, $suffix, $separator, $column ) =
3770 _condensed_generator_defaults($format);
3772 my $money_char = '$';
3773 if ($format eq 'latex') {
3774 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3776 $separator = " & \n";
3778 sub { my ($d,$a,$s,$w) = @_;
3779 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3781 $money_char = '\\dollar';
3782 }elsif ( $format eq 'html' ) {
3783 $prefix = '"><td align="center"></td>';
3787 sub { my ($d,$a,$s,$w) = @_;
3788 return qq!<td align="$html_align{$a}">$d</td>!;
3790 #$money_char = $conf->config('money_char') || '$';
3791 $money_char = ''; # this is madness
3799 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3801 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3803 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3804 map { $f->{$_}->[$i] } qw(align span width)
3808 $prefix. join( $separator, @result ). $suffix;
3813 sub _condensed_total_generator {
3814 my ( $self, $format ) = ( shift, shift );
3816 my ( $f, $prefix, $suffix, $separator, $column ) =
3817 _condensed_generator_defaults($format);
3820 if ($format eq 'latex') {
3823 $separator = " & \n";
3825 sub { my ($d,$a,$s,$w) = @_;
3826 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3828 }elsif ( $format eq 'html' ) {
3832 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3834 sub { my ($d,$a,$s,$w) = @_;
3835 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3844 # my $r = &{$f->{fields}->[$i]}(@args);
3845 # $r .= ' Total' unless $i;
3847 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3849 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3850 map { $f->{$_}->[$i] } qw(align span width)
3854 $prefix. join( $separator, @result ). $suffix;
3859 =item total_line_generator FORMAT
3861 Returns a coderef used for generation of invoice total line items for this
3862 usage_class. FORMAT is either html or latex
3866 # should not be used: will have issues with hash element names (description vs
3867 # total_item and amount vs total_amount -- another array of functions?
3869 sub _condensed_total_line_generator {
3870 my ( $self, $format ) = ( shift, shift );
3872 my ( $f, $prefix, $suffix, $separator, $column ) =
3873 _condensed_generator_defaults($format);
3876 if ($format eq 'latex') {
3879 $separator = " & \n";
3881 sub { my ($d,$a,$s,$w) = @_;
3882 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3884 }elsif ( $format eq 'html' ) {
3888 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3890 sub { my ($d,$a,$s,$w) = @_;
3891 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3900 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3902 &{$column}( &{$f->{fields}->[$i]}(@args),
3903 map { $f->{$_}->[$i] } qw(align span width)
3907 $prefix. join( $separator, @result ). $suffix;
3912 #sub _items_extra_usage_sections {
3914 # my $escape = shift;
3916 # my %sections = ();
3918 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3919 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3921 # next unless $cust_bill_pkg->pkgnum > 0;
3923 # foreach my $section ( keys %usage_class ) {
3925 # my $usage = $cust_bill_pkg->usage($section);
3927 # next unless $usage && $usage > 0;
3929 # $sections{$section} ||= 0;
3930 # $sections{$section} += $usage;
3936 # map { { 'description' => &{$escape}($_),
3937 # 'subtotal' => $sections{$_},
3938 # 'summarized' => '',
3939 # 'tax_section' => '',
3942 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3946 sub _items_extra_usage_sections {
3955 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3956 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3957 next unless $cust_bill_pkg->pkgnum > 0;
3959 foreach my $classnum ( keys %usage_class ) {
3960 my $section = $usage_class{$classnum}->classname;
3961 $classnums{$section} = $classnum;
3963 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3964 my $amount = $detail->amount;
3965 next unless $amount && $amount > 0;
3967 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3968 $sections{$section}{amount} += $amount; #subtotal
3969 $sections{$section}{calls}++;
3970 $sections{$section}{duration} += $detail->duration;
3972 my $desc = $detail->regionname;
3973 my $description = $desc;
3974 $description = substr($desc, 0, 50). '...'
3975 if $format eq 'latex' && length($desc) > 50;
3977 $lines{$section}{$desc} ||= {
3978 description => &{$escape}($description),
3979 #pkgpart => $part_pkg->pkgpart,
3980 pkgnum => $cust_bill_pkg->pkgnum,
3985 #unit_amount => $cust_bill_pkg->unitrecur,
3986 quantity => $cust_bill_pkg->quantity,
3987 product_code => 'N/A',
3988 ext_description => [],
3991 $lines{$section}{$desc}{amount} += $amount;
3992 $lines{$section}{$desc}{calls}++;
3993 $lines{$section}{$desc}{duration} += $detail->duration;
3999 my %sectionmap = ();
4000 foreach (keys %sections) {
4001 my $usage_class = $usage_class{$classnums{$_}};
4002 $sectionmap{$_} = { 'description' => &{$escape}($_),
4003 'amount' => $sections{$_}{amount}, #subtotal
4004 'calls' => $sections{$_}{calls},
4005 'duration' => $sections{$_}{duration},
4007 'tax_section' => '',
4008 'sort_weight' => $usage_class->weight,
4009 ( $usage_class->format
4010 ? ( map { $_ => $usage_class->$_($format) }
4011 qw( description_generator header_generator total_generator total_line_generator )
4018 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4022 foreach my $section ( keys %lines ) {
4023 foreach my $line ( keys %{$lines{$section}} ) {
4024 my $l = $lines{$section}{$line};
4025 $l->{section} = $sectionmap{$section};
4026 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4027 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4032 return(\@sections, \@lines);
4038 my $end = $self->_date;
4040 # start at date of previous invoice + 1 second or 0 if no previous invoice
4041 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4042 $start = 0 if !$start;
4045 my $cust_main = $self->cust_main;
4046 my @pkgs = $cust_main->all_pkgs;
4047 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4050 foreach my $pkg ( @pkgs ) {
4051 my @h_cust_svc = $pkg->h_cust_svc($end);
4052 foreach my $h_cust_svc ( @h_cust_svc ) {
4053 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4054 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4056 my $inserted = $h_cust_svc->date_inserted;
4057 my $deleted = $h_cust_svc->date_deleted;
4058 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4060 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4062 # DID either activated or ported in; cannot be both for same DID simultaneously
4063 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4064 && (!$phone_inserted->lnp_status
4065 || $phone_inserted->lnp_status eq ''
4066 || $phone_inserted->lnp_status eq 'native')) {
4069 else { # this one not so clean, should probably move to (h_)svc_phone
4070 my $phone_portedin = qsearchs( 'h_svc_phone',
4071 { 'svcnum' => $h_cust_svc->svcnum,
4072 'lnp_status' => 'portedin' },
4073 FS::h_svc_phone->sql_h_searchs($end),
4075 $num_portedin++ if $phone_portedin;
4078 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4079 if($deleted >= $start && $deleted <= $end && $phone_deleted
4080 && (!$phone_deleted->lnp_status
4081 || $phone_deleted->lnp_status ne 'portingout')) {
4084 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4085 && $phone_deleted->lnp_status
4086 && $phone_deleted->lnp_status eq 'portingout') {
4090 # increment usage minutes
4091 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4092 foreach my $cdr ( @cdrs ) {
4093 $minutes += $cdr->billsec/60;
4096 # don't look at this service again
4097 push @seen, $h_cust_svc->svcnum;
4101 $minutes = sprintf("%d", $minutes);
4102 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4103 . "$num_deactivated Ported-Out: $num_portedout ",
4104 "Total Minutes: $minutes");
4107 sub _items_svc_phone_sections {
4116 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4117 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4119 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4120 next unless $cust_bill_pkg->pkgnum > 0;
4122 my @header = $cust_bill_pkg->details_header;
4123 next unless scalar(@header);
4125 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4127 my $phonenum = $detail->phonenum;
4128 next unless $phonenum;
4130 my $amount = $detail->amount;
4131 next unless $amount && $amount > 0;
4133 $sections{$phonenum} ||= { 'amount' => 0,
4136 'sort_weight' => -1,
4137 'phonenum' => $phonenum,
4139 $sections{$phonenum}{amount} += $amount; #subtotal
4140 $sections{$phonenum}{calls}++;
4141 $sections{$phonenum}{duration} += $detail->duration;
4143 my $desc = $detail->regionname;
4144 my $description = $desc;
4145 $description = substr($desc, 0, 50). '...'
4146 if $format eq 'latex' && length($desc) > 50;
4148 $lines{$phonenum}{$desc} ||= {
4149 description => &{$escape}($description),
4150 #pkgpart => $part_pkg->pkgpart,
4158 product_code => 'N/A',
4159 ext_description => [],
4162 $lines{$phonenum}{$desc}{amount} += $amount;
4163 $lines{$phonenum}{$desc}{calls}++;
4164 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4166 my $line = $usage_class{$detail->classnum}->classname;
4167 $sections{"$phonenum $line"} ||=
4171 'sort_weight' => $usage_class{$detail->classnum}->weight,
4172 'phonenum' => $phonenum,
4173 'header' => [ @header ],
4175 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4176 $sections{"$phonenum $line"}{calls}++;
4177 $sections{"$phonenum $line"}{duration} += $detail->duration;
4179 $lines{"$phonenum $line"}{$desc} ||= {
4180 description => &{$escape}($description),
4181 #pkgpart => $part_pkg->pkgpart,
4189 product_code => 'N/A',
4190 ext_description => [],
4193 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4194 $lines{"$phonenum $line"}{$desc}{calls}++;
4195 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4196 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4197 $detail->formatted('format' => $format);
4202 my %sectionmap = ();
4203 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4204 foreach ( keys %sections ) {
4205 my @header = @{ $sections{$_}{header} || [] };
4207 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4208 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4209 my $usage_class = $summary ? $simple : $usage_simple;
4210 my $ending = $summary ? ' usage charges' : '';
4213 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4215 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4216 'amount' => $sections{$_}{amount}, #subtotal
4217 'calls' => $sections{$_}{calls},
4218 'duration' => $sections{$_}{duration},
4220 'tax_section' => '',
4221 'phonenum' => $sections{$_}{phonenum},
4222 'sort_weight' => $sections{$_}{sort_weight},
4223 'post_total' => $summary, #inspire pagebreak
4225 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4226 qw( description_generator
4229 total_line_generator
4236 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4237 $a->{sort_weight} <=> $b->{sort_weight}
4242 foreach my $section ( keys %lines ) {
4243 foreach my $line ( keys %{$lines{$section}} ) {
4244 my $l = $lines{$section}{$line};
4245 $l->{section} = $sectionmap{$section};
4246 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4247 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4252 if($conf->exists('phone_usage_class_summary')) {
4253 # this only works with Latex
4257 # after this, we'll have only two sections per DID:
4258 # Calls Summary and Calls Detail
4259 foreach my $section ( @sections ) {
4260 if($section->{'post_total'}) {
4261 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4262 $section->{'total_line_generator'} = sub { '' };
4263 $section->{'total_generator'} = sub { '' };
4264 $section->{'header_generator'} = sub { '' };
4265 $section->{'description_generator'} = '';
4266 push @newsections, $section;
4267 my %calls_detail = %$section;
4268 $calls_detail{'post_total'} = '';
4269 $calls_detail{'sort_weight'} = '';
4270 $calls_detail{'description_generator'} = sub { '' };
4271 $calls_detail{'header_generator'} = sub {
4272 return ' & Date/Time & Called Number & Duration & Price'
4273 if $format eq 'latex';
4276 $calls_detail{'description'} = 'Calls Detail: '
4277 . $section->{'phonenum'};
4278 push @newsections, \%calls_detail;
4282 # after this, each usage class is collapsed/summarized into a single
4283 # line under the Calls Summary section
4284 foreach my $newsection ( @newsections ) {
4285 if($newsection->{'post_total'}) { # this means Calls Summary
4286 foreach my $section ( @sections ) {
4287 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4288 && !$section->{'post_total'});
4289 my $newdesc = $section->{'description'};
4290 my $tn = $section->{'phonenum'};
4291 $newdesc =~ s/$tn//g;
4292 my $line = { ext_description => [],
4296 calls => $section->{'calls'},
4297 section => $newsection,
4298 duration => $section->{'duration'},
4299 description => $newdesc,
4300 amount => sprintf("%.2f",$section->{'amount'}),
4301 product_code => 'N/A',
4303 push @newlines, $line;
4308 # after this, Calls Details is populated with all CDRs
4309 foreach my $newsection ( @newsections ) {
4310 if(!$newsection->{'post_total'}) { # this means Calls Details
4311 foreach my $line ( @lines ) {
4312 next unless (scalar(@{$line->{'ext_description'}}) &&
4313 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4315 my @extdesc = @{$line->{'ext_description'}};
4317 foreach my $extdesc ( @extdesc ) {
4318 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4319 push @newextdesc, $extdesc;
4321 $line->{'ext_description'} = \@newextdesc;
4322 $line->{'section'} = $newsection;
4323 push @newlines, $line;
4328 return(\@newsections, \@newlines);
4331 return(\@sections, \@lines);
4338 #my @display = scalar(@_)
4340 # : qw( _items_previous _items_pkg );
4341 # #: qw( _items_pkg );
4342 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4343 my @display = qw( _items_previous _items_pkg );
4346 foreach my $display ( @display ) {
4347 push @b, $self->$display(@_);
4352 sub _items_previous {
4354 my $cust_main = $self->cust_main;
4355 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4357 foreach ( @pr_cust_bill ) {
4358 my $date = $conf->exists('invoice_show_prior_due_date')
4359 ? 'due '. $_->due_date2str($date_format)
4360 : time2str($date_format, $_->_date);
4362 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4363 #'pkgpart' => 'N/A',
4365 'amount' => sprintf("%.2f", $_->owed),
4371 # 'description' => 'Previous Balance',
4372 # #'pkgpart' => 'N/A',
4373 # 'pkgnum' => 'N/A',
4374 # 'amount' => sprintf("%10.2f", $pr_total ),
4375 # 'ext_description' => [ map {
4376 # "Invoice ". $_->invnum.
4377 # " (". time2str("%x",$_->_date). ") ".
4378 # sprintf("%10.2f", $_->owed)
4379 # } @pr_cust_bill ],
4388 warn "$me _items_pkg searching for all package line items\n"
4391 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4393 warn "$me _items_pkg filtering line items\n"
4395 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4397 if ($options{section} && $options{section}->{condensed}) {
4399 warn "$me _items_pkg condensing section\n"
4403 local $Storable::canonical = 1;
4404 foreach ( @items ) {
4406 delete $item->{ref};
4407 delete $item->{ext_description};
4408 my $key = freeze($item);
4409 $itemshash{$key} ||= 0;
4410 $itemshash{$key} ++; # += $item->{quantity};
4412 @items = sort { $a->{description} cmp $b->{description} }
4413 map { my $i = thaw($_);
4414 $i->{quantity} = $itemshash{$_};
4416 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4422 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4429 return 0 unless $a->itemdesc cmp $b->itemdesc;
4430 return -1 if $b->itemdesc eq 'Tax';
4431 return 1 if $a->itemdesc eq 'Tax';
4432 return -1 if $b->itemdesc eq 'Other surcharges';
4433 return 1 if $a->itemdesc eq 'Other surcharges';
4434 $a->itemdesc cmp $b->itemdesc;
4439 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4440 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4443 sub _items_cust_bill_pkg {
4445 my $cust_bill_pkgs = shift;
4448 my $format = $opt{format} || '';
4449 my $escape_function = $opt{escape_function} || sub { shift };
4450 my $format_function = $opt{format_function} || '';
4451 my $unsquelched = $opt{unsquelched} || '';
4452 my $section = $opt{section}->{description} if $opt{section};
4453 my $summary_page = $opt{summary_page} || '';
4454 my $multilocation = $opt{multilocation} || '';
4455 my $multisection = $opt{multisection} || '';
4456 my $discount_show_always = 0;
4459 my ($s, $r, $u) = ( undef, undef, undef );
4460 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4463 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4464 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4467 foreach my $display ( grep { defined($section)
4468 ? $_->section eq $section
4471 #grep { !$_->summary || !$summary_page } # bunk!
4472 grep { !$_->summary || $multisection }
4473 $cust_bill_pkg->cust_bill_pkg_display
4477 warn "$me _items_cust_bill_pkg considering display item $display\n"
4480 my $type = $display->type;
4482 my $desc = $cust_bill_pkg->desc;
4483 $desc = substr($desc, 0, 50). '...'
4484 if $format eq 'latex' && length($desc) > 50;
4486 my %details_opt = ( 'format' => $format,
4487 'escape_function' => $escape_function,
4488 'format_function' => $format_function,
4491 if ( $cust_bill_pkg->pkgnum > 0 ) {
4493 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4496 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4498 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4500 warn "$me _items_cust_bill_pkg adding setup\n"
4503 my $description = $desc;
4504 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4507 unless ( $cust_pkg->part_pkg->hide_svc_detail
4508 || $cust_bill_pkg->hidden )
4511 push @d, map &{$escape_function}($_),
4512 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4513 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4515 if ( $multilocation ) {
4516 my $loc = $cust_pkg->location_label;
4517 $loc = substr($loc, 0, 50). '...'
4518 if $format eq 'latex' && length($loc) > 50;
4519 push @d, &{$escape_function}($loc);
4524 push @d, $cust_bill_pkg->details(%details_opt)
4525 if $cust_bill_pkg->recur == 0;
4527 if ( $cust_bill_pkg->hidden ) {
4528 $s->{amount} += $cust_bill_pkg->setup;
4529 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4530 push @{ $s->{ext_description} }, @d;
4533 description => $description,
4534 #pkgpart => $part_pkg->pkgpart,
4535 pkgnum => $cust_bill_pkg->pkgnum,
4536 amount => $cust_bill_pkg->setup,
4537 unit_amount => $cust_bill_pkg->unitsetup,
4538 quantity => $cust_bill_pkg->quantity,
4539 ext_description => \@d,
4545 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4547 $cust_bill_pkg->recur != 0
4548 || $cust_bill_pkg->setup == 0
4549 || $discount_show_always
4550 || $cust_bill_pkg->recur_show_zero
4555 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4558 my $is_summary = $display->summary;
4559 my $description = ($is_summary && $type && $type eq 'U')
4560 ? "Usage charges" : $desc;
4562 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4563 " - ". time2str($date_format, $cust_bill_pkg->edate).
4565 unless $conf->exists('disable_line_item_date_ranges');
4569 #at least until cust_bill_pkg has "past" ranges in addition to
4570 #the "future" sdate/edate ones... see #3032
4571 my @dates = ( $self->_date );
4572 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4573 push @dates, $prev->sdate if $prev;
4574 push @dates, undef if !$prev;
4576 unless ( $cust_pkg->part_pkg->hide_svc_detail
4577 || $cust_bill_pkg->itemdesc
4578 || $cust_bill_pkg->hidden
4579 || $is_summary && $type && $type eq 'U' )
4582 warn "$me _items_cust_bill_pkg adding service details\n"
4585 push @d, map &{$escape_function}($_),
4586 $cust_pkg->h_labels_short(@dates, 'I')
4587 #$cust_bill_pkg->edate,
4588 #$cust_bill_pkg->sdate)
4589 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4591 warn "$me _items_cust_bill_pkg done adding service details\n"
4594 if ( $multilocation ) {
4595 my $loc = $cust_pkg->location_label;
4596 $loc = substr($loc, 0, 50). '...'
4597 if $format eq 'latex' && length($loc) > 50;
4598 push @d, &{$escape_function}($loc);
4603 unless ( $is_summary ) {
4604 warn "$me _items_cust_bill_pkg adding details\n"
4607 #instead of omitting details entirely in this case (unwanted side
4608 # effects), just omit CDRs
4609 $details_opt{'format_function'} = sub { () }
4610 if $type && $type eq 'R';
4612 push @d, $cust_bill_pkg->details(%details_opt);
4615 warn "$me _items_cust_bill_pkg calculating amount\n"
4620 $amount = $cust_bill_pkg->recur;
4621 } elsif ($type eq 'R') {
4622 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4623 } elsif ($type eq 'U') {
4624 $amount = $cust_bill_pkg->usage;
4627 if ( !$type || $type eq 'R' ) {
4629 warn "$me _items_cust_bill_pkg adding recur\n"
4632 if ( $cust_bill_pkg->hidden ) {
4633 $r->{amount} += $amount;
4634 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4635 push @{ $r->{ext_description} }, @d;
4638 description => $description,
4639 #pkgpart => $part_pkg->pkgpart,
4640 pkgnum => $cust_bill_pkg->pkgnum,
4642 unit_amount => $cust_bill_pkg->unitrecur,
4643 quantity => $cust_bill_pkg->quantity,
4644 ext_description => \@d,
4648 } else { # $type eq 'U'
4650 warn "$me _items_cust_bill_pkg adding usage\n"
4653 if ( $cust_bill_pkg->hidden ) {
4654 $u->{amount} += $amount;
4655 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4656 push @{ $u->{ext_description} }, @d;
4659 description => $description,
4660 #pkgpart => $part_pkg->pkgpart,
4661 pkgnum => $cust_bill_pkg->pkgnum,
4663 unit_amount => $cust_bill_pkg->unitrecur,
4664 quantity => $cust_bill_pkg->quantity,
4665 ext_description => \@d,
4671 } # recurring or usage with recurring charge
4673 } else { #pkgnum tax or one-shot line item (??)
4675 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4678 if ( $cust_bill_pkg->setup != 0 ) {
4680 'description' => $desc,
4681 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4684 if ( $cust_bill_pkg->recur != 0 ) {
4686 'description' => "$desc (".
4687 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4688 time2str($date_format, $cust_bill_pkg->edate). ')',
4689 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4697 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4698 && $conf->exists('discount-show-always'));
4700 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4701 if ( $_ && !$cust_bill_pkg->hidden ) {
4702 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4703 $_->{amount} =~ s/^\-0\.00$/0.00/;
4704 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4706 if $_->{amount} != 0
4707 || $discount_show_always
4708 || $cust_bill_pkg->recur_show_zero;
4715 #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4717 # $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4718 # $_->{amount} =~ s/^\-0\.00$/0.00/;
4719 # $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4721 # if $_->{amount} != 0
4722 # || $discount_show_always
4726 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4733 sub _items_credits {
4734 my( $self, %opt ) = @_;
4735 my $trim_len = $opt{'trim_len'} || 60;
4739 foreach ( $self->cust_credited ) {
4741 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4743 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4744 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4745 $reason = " ($reason) " if $reason;
4748 #'description' => 'Credit ref\#'. $_->crednum.
4749 # " (". time2str("%x",$_->cust_credit->_date) .")".
4751 'description' => 'Credit applied '.
4752 time2str($date_format,$_->cust_credit->_date). $reason,
4753 'amount' => sprintf("%.2f",$_->amount),
4761 sub _items_payments {
4765 #get & print payments
4766 foreach ( $self->cust_bill_pay ) {
4768 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4771 'description' => "Payment received ".
4772 time2str($date_format,$_->cust_pay->_date ),
4773 'amount' => sprintf("%.2f", $_->amount )
4781 =item call_details [ OPTION => VALUE ... ]
4783 Returns an array of CSV strings representing the call details for this invoice
4784 The only option available is the boolean prepend_billed_number
4789 my ($self, %opt) = @_;
4791 my $format_function = sub { shift };
4793 if ($opt{prepend_billed_number}) {
4794 $format_function = sub {
4798 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4803 my @details = map { $_->details( 'format_function' => $format_function,
4804 'escape_function' => sub{ return() },
4808 $self->cust_bill_pkg;
4809 my $header = $details[0];
4810 ( $header, grep { $_ ne $header } @details );
4820 =item process_reprint
4824 sub process_reprint {
4825 process_re_X('print', @_);
4828 =item process_reemail
4832 sub process_reemail {
4833 process_re_X('email', @_);
4841 process_re_X('fax', @_);
4849 process_re_X('ftp', @_);
4856 sub process_respool {
4857 process_re_X('spool', @_);
4860 use Storable qw(thaw);
4864 my( $method, $job ) = ( shift, shift );
4865 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4867 my $param = thaw(decode_base64(shift));
4868 warn Dumper($param) if $DEBUG;
4879 my($method, $job, %param ) = @_;
4881 warn "re_X $method for job $job with param:\n".
4882 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4885 #some false laziness w/search/cust_bill.html
4887 my $orderby = 'ORDER BY cust_bill._date';
4889 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4891 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4893 my @cust_bill = qsearch( {
4894 #'select' => "cust_bill.*",
4895 'table' => 'cust_bill',
4896 'addl_from' => $addl_from,
4898 'extra_sql' => $extra_sql,
4899 'order_by' => $orderby,
4903 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4905 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4908 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4909 foreach my $cust_bill ( @cust_bill ) {
4910 $cust_bill->$method();
4912 if ( $job ) { #progressbar foo
4914 if ( time - $min_sec > $last ) {
4915 my $error = $job->update_statustext(
4916 int( 100 * $num / scalar(@cust_bill) )
4918 die $error if $error;
4929 =head1 CLASS METHODS
4935 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4940 my ($class, $start, $end) = @_;
4942 $class->paid_sql($start, $end). ' - '.
4943 $class->credited_sql($start, $end);
4948 Returns an SQL fragment to retreive the net amount (charged minus credited).
4953 my ($class, $start, $end) = @_;
4954 'charged - '. $class->credited_sql($start, $end);
4959 Returns an SQL fragment to retreive the amount paid against this invoice.
4964 my ($class, $start, $end) = @_;
4965 $start &&= "AND cust_bill_pay._date <= $start";
4966 $end &&= "AND cust_bill_pay._date > $end";
4967 $start = '' unless defined($start);
4968 $end = '' unless defined($end);
4969 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4970 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
4975 Returns an SQL fragment to retreive the amount credited against this invoice.
4980 my ($class, $start, $end) = @_;
4981 $start &&= "AND cust_credit_bill._date <= $start";
4982 $end &&= "AND cust_credit_bill._date > $end";
4983 $start = '' unless defined($start);
4984 $end = '' unless defined($end);
4985 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4986 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
4991 Returns an SQL fragment to retrieve the due date of an invoice.
4992 Currently only supported on PostgreSQL.
5000 cust_bill.invoice_terms,
5001 cust_main.invoice_terms,
5002 \''.($conf->config('invoice_default_terms') || '').'\'
5003 ), E\'Net (\\\\d+)\'
5005 ) * 86400 + cust_bill._date'
5008 =item search_sql_where HASHREF
5010 Class method which returns an SQL WHERE fragment to search for parameters
5011 specified in HASHREF. Valid parameters are
5017 List reference of start date, end date, as UNIX timestamps.
5027 List reference of charged limits (exclusive).
5031 List reference of charged limits (exclusive).
5035 flag, return open invoices only
5039 flag, return net invoices only
5043 =item newest_percust
5047 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5051 sub search_sql_where {
5052 my($class, $param) = @_;
5054 warn "$me search_sql_where called with params: \n".
5055 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5061 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5062 push @search, "cust_main.agentnum = $1";
5066 if ( $param->{_date} ) {
5067 my($beginning, $ending) = @{$param->{_date}};
5069 push @search, "cust_bill._date >= $beginning",
5070 "cust_bill._date < $ending";
5074 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5075 push @search, "cust_bill.invnum >= $1";
5077 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5078 push @search, "cust_bill.invnum <= $1";
5082 if ( $param->{charged} ) {
5083 my @charged = ref($param->{charged})
5084 ? @{ $param->{charged} }
5085 : ($param->{charged});
5087 push @search, map { s/^charged/cust_bill.charged/; $_; }
5091 my $owed_sql = FS::cust_bill->owed_sql;
5094 if ( $param->{owed} ) {
5095 my @owed = ref($param->{owed})
5096 ? @{ $param->{owed} }
5098 push @search, map { s/^owed/$owed_sql/; $_; }
5103 push @search, "0 != $owed_sql"
5104 if $param->{'open'};
5105 push @search, '0 != '. FS::cust_bill->net_sql
5109 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5110 if $param->{'days'};
5113 if ( $param->{'newest_percust'} ) {
5115 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5116 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5118 my @newest_where = map { my $x = $_;
5119 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5122 grep ! /^cust_main./, @search;
5123 my $newest_where = scalar(@newest_where)
5124 ? ' AND '. join(' AND ', @newest_where)
5128 push @search, "cust_bill._date = (
5129 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5130 WHERE newest_cust_bill.custnum = cust_bill.custnum
5136 #agent virtualization
5137 my $curuser = $FS::CurrentUser::CurrentUser;
5138 if ( $curuser->username eq 'fs_queue'
5139 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5141 my $newuser = qsearchs('access_user', {
5142 'username' => $username,
5146 $curuser = $newuser;
5148 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5151 push @search, $curuser->agentnums_sql;
5153 join(' AND ', @search );
5165 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5166 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base