4 use vars qw( @ISA $DEBUG $me $conf
5 $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
9 use List::Util qw(min max);
11 use Text::Template 1.20;
13 use String::ShellQuote;
16 use Storable qw( freeze thaw );
18 use FS::UID qw( datasrc );
19 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
20 use FS::Record qw( qsearch qsearchs dbh );
21 use FS::cust_main_Mixin;
23 use FS::cust_statement;
24 use FS::cust_bill_pkg;
25 use FS::cust_bill_pkg_display;
26 use FS::cust_bill_pkg_detail;
30 use FS::cust_credit_bill;
32 use FS::cust_pay_batch;
33 use FS::cust_bill_event;
36 use FS::cust_bill_pay;
37 use FS::cust_bill_pay_batch;
38 use FS::part_bill_event;
41 use FS::cust_bill_batch;
42 use FS::cust_bill_pay_pkg;
43 use FS::cust_credit_bill_pkg;
45 @ISA = qw( FS::cust_main_Mixin FS::Record );
48 $me = '[FS::cust_bill]';
50 #ask FS::UID to run this stuff for us later
51 FS::UID->install_callback( sub {
53 $money_char = $conf->config('money_char') || '$';
54 $date_format = $conf->config('date_format') || '%x'; #/YY
55 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
56 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
61 FS::cust_bill - Object methods for cust_bill records
67 $record = new FS::cust_bill \%hash;
68 $record = new FS::cust_bill { 'column' => 'value' };
70 $error = $record->insert;
72 $error = $new_record->replace($old_record);
74 $error = $record->delete;
76 $error = $record->check;
78 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
80 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
82 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
84 @cust_pay_objects = $cust_bill->cust_pay;
86 $tax_amount = $record->tax;
88 @lines = $cust_bill->print_text;
89 @lines = $cust_bill->print_text $time;
93 An FS::cust_bill object represents an invoice; a declaration that a customer
94 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
95 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
96 following fields are currently supported:
102 =item invnum - primary key (assigned automatically for new invoices)
104 =item custnum - customer (see L<FS::cust_main>)
106 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
107 L<Time::Local> and L<Date::Parse> for conversion functions.
109 =item charged - amount of this invoice
111 =item invoice_terms - optional terms override for this specific invoice
115 Customer info at invoice generation time
119 =item previous_balance
121 =item billing_balance
129 =item printed - deprecated
137 =item closed - books closed flag, empty or `Y'
139 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
141 =item agent_invid - legacy invoice number
151 Creates a new invoice. To add the invoice to the database, see L<"insert">.
152 Invoices are normally created by calling the bill method of a customer object
153 (see L<FS::cust_main>).
157 sub table { 'cust_bill'; }
159 sub cust_linked { $_[0]->cust_main_custnum; }
160 sub cust_unlinked_msg {
162 "WARNING: can't find cust_main.custnum ". $self->custnum.
163 ' (cust_bill.invnum '. $self->invnum. ')';
168 Adds this invoice to the database ("Posts" the invoice). If there is an error,
169 returns the error, otherwise returns false.
175 warn "$me insert called\n" if $DEBUG;
177 local $SIG{HUP} = 'IGNORE';
178 local $SIG{INT} = 'IGNORE';
179 local $SIG{QUIT} = 'IGNORE';
180 local $SIG{TERM} = 'IGNORE';
181 local $SIG{TSTP} = 'IGNORE';
182 local $SIG{PIPE} = 'IGNORE';
184 my $oldAutoCommit = $FS::UID::AutoCommit;
185 local $FS::UID::AutoCommit = 0;
188 my $error = $self->SUPER::insert;
190 $dbh->rollback if $oldAutoCommit;
194 if ( $self->get('cust_bill_pkg') ) {
195 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
196 $cust_bill_pkg->invnum($self->invnum);
197 my $error = $cust_bill_pkg->insert;
199 $dbh->rollback if $oldAutoCommit;
200 return "can't create invoice line item: $error";
205 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
212 This method now works but you probably shouldn't use it. Instead, apply a
213 credit against the invoice.
215 Using this method to delete invoices outright is really, really bad. There
216 would be no record you ever posted this invoice, and there are no check to
217 make sure charged = 0 or that there are no associated cust_bill_pkg records.
219 Really, don't use it.
225 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
227 local $SIG{HUP} = 'IGNORE';
228 local $SIG{INT} = 'IGNORE';
229 local $SIG{QUIT} = 'IGNORE';
230 local $SIG{TERM} = 'IGNORE';
231 local $SIG{TSTP} = 'IGNORE';
232 local $SIG{PIPE} = 'IGNORE';
234 my $oldAutoCommit = $FS::UID::AutoCommit;
235 local $FS::UID::AutoCommit = 0;
238 foreach my $table (qw(
250 foreach my $linked ( $self->$table() ) {
251 my $error = $linked->delete;
253 $dbh->rollback if $oldAutoCommit;
260 my $error = $self->SUPER::delete(@_);
262 $dbh->rollback if $oldAutoCommit;
266 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
272 =item replace [ OLD_RECORD ]
274 You can, but probably shouldn't modify invoices...
276 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
277 supplied, replaces this record. If there is an error, returns the error,
278 otherwise returns false.
282 #replace can be inherited from Record.pm
284 # replace_check is now the preferred way to #implement replace data checks
285 # (so $object->replace() works without an argument)
288 my( $new, $old ) = ( shift, shift );
289 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
290 #return "Can't change _date!" unless $old->_date eq $new->_date;
291 return "Can't change _date" unless $old->_date == $new->_date;
292 return "Can't change charged" unless $old->charged == $new->charged
293 || $old->charged == 0
294 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
300 =item add_cc_surcharge
306 sub add_cc_surcharge {
307 my ($self, $pkgnum, $amount) = (shift, shift, shift);
310 my $cust_bill_pkg = new FS::cust_bill_pkg({
311 'invnum' => $self->invnum,
315 $error = $cust_bill_pkg->insert;
316 return $error if $error;
318 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
319 $self->charged($self->charged+$amount);
320 $error = $self->replace;
321 return $error if $error;
323 $self->apply_payments_and_credits;
329 Checks all fields to make sure this is a valid invoice. If there is an error,
330 returns the error, otherwise returns false. Called by the insert and replace
339 $self->ut_numbern('invnum')
340 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
341 || $self->ut_numbern('_date')
342 || $self->ut_money('charged')
343 || $self->ut_numbern('printed')
344 || $self->ut_enum('closed', [ '', 'Y' ])
345 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
346 || $self->ut_numbern('agent_invid') #varchar?
348 return $error if $error;
350 $self->_date(time) unless $self->_date;
352 $self->printed(0) if $self->printed eq '';
359 Returns the displayed invoice number for this invoice: agent_invid if
360 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
366 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
367 return $self->agent_invid;
369 return $self->invnum;
375 Returns a list consisting of the total previous balance for this customer,
376 followed by the previous outstanding invoices (as FS::cust_bill objects also).
383 my @cust_bill = sort { $a->_date <=> $b->_date }
384 grep { $_->owed != 0 && $_->_date < $self->_date }
385 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
387 foreach ( @cust_bill ) { $total += $_->owed; }
393 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
400 { 'table' => 'cust_bill_pkg',
401 'hashref' => { 'invnum' => $self->invnum },
402 'order_by' => 'ORDER BY billpkgnum',
407 =item cust_bill_pkg_pkgnum PKGNUM
409 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
414 sub cust_bill_pkg_pkgnum {
415 my( $self, $pkgnum ) = @_;
417 { 'table' => 'cust_bill_pkg',
418 'hashref' => { 'invnum' => $self->invnum,
421 'order_by' => 'ORDER BY billpkgnum',
428 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
435 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
436 $self->cust_bill_pkg;
438 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
443 Returns true if any of the packages (or their definitions) corresponding to the
444 line items for this invoice have the no_auto flag set.
450 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
453 =item open_cust_bill_pkg
455 Returns the open line items for this invoice.
457 Note that cust_bill_pkg with both setup and recur fees are returned as two
458 separate line items, each with only one fee.
462 # modeled after cust_main::open_cust_bill
463 sub open_cust_bill_pkg {
466 # grep { $_->owed > 0 } $self->cust_bill_pkg
468 my %other = ( 'recur' => 'setup',
469 'setup' => 'recur', );
471 foreach my $field ( qw( recur setup )) {
472 push @open, map { $_->set( $other{$field}, 0 ); $_; }
473 grep { $_->owed($field) > 0 }
474 $self->cust_bill_pkg;
480 =item cust_bill_event
482 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
486 sub cust_bill_event {
488 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
491 =item num_cust_bill_event
493 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
497 sub num_cust_bill_event {
500 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
501 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
502 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
503 $sth->fetchrow_arrayref->[0];
508 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
512 #false laziness w/cust_pkg.pm
516 'table' => 'cust_event',
517 'addl_from' => 'JOIN part_event USING ( eventpart )',
518 'hashref' => { 'tablenum' => $self->invnum },
519 'extra_sql' => " AND eventtable = 'cust_bill' ",
525 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
529 #false laziness w/cust_pkg.pm
533 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
534 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
535 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
536 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
537 $sth->fetchrow_arrayref->[0];
542 Returns the customer (see L<FS::cust_main>) for this invoice.
548 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
551 =item cust_suspend_if_balance_over AMOUNT
553 Suspends the customer associated with this invoice if the total amount owed on
554 this invoice and all older invoices is greater than the specified amount.
556 Returns a list: an empty list on success or a list of errors.
560 sub cust_suspend_if_balance_over {
561 my( $self, $amount ) = ( shift, shift );
562 my $cust_main = $self->cust_main;
563 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
566 $cust_main->suspend(@_);
572 Depreciated. See the cust_credited method.
574 #Returns a list consisting of the total previous credited (see
575 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
576 #outstanding credits (FS::cust_credit objects).
582 croak "FS::cust_bill->cust_credit depreciated; see ".
583 "FS::cust_bill->cust_credit_bill";
586 #my @cust_credit = sort { $a->_date <=> $b->_date }
587 # grep { $_->credited != 0 && $_->_date < $self->_date }
588 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
590 #foreach (@cust_credit) { $total += $_->credited; }
591 #$total, @cust_credit;
596 Depreciated. See the cust_bill_pay method.
598 #Returns all payments (see L<FS::cust_pay>) for this invoice.
604 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
606 #sort { $a->_date <=> $b->_date }
607 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
613 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
616 sub cust_bill_pay_batch {
618 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
623 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
629 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
630 sort { $a->_date <=> $b->_date }
631 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
636 =item cust_credit_bill
638 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
644 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
645 sort { $a->_date <=> $b->_date }
646 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
650 sub cust_credit_bill {
651 shift->cust_credited(@_);
654 #=item cust_bill_pay_pkgnum PKGNUM
656 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
657 #with matching pkgnum.
661 #sub cust_bill_pay_pkgnum {
662 # my( $self, $pkgnum ) = @_;
663 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
664 # sort { $a->_date <=> $b->_date }
665 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
666 # 'pkgnum' => $pkgnum,
671 =item cust_bill_pay_pkg PKGNUM
673 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
674 applied against the matching pkgnum.
678 sub cust_bill_pay_pkg {
679 my( $self, $pkgnum ) = @_;
682 'select' => 'cust_bill_pay_pkg.*',
683 'table' => 'cust_bill_pay_pkg',
684 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
685 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
686 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
687 " AND cust_bill_pkg.pkgnum = $pkgnum",
692 #=item cust_credited_pkgnum PKGNUM
694 #=item cust_credit_bill_pkgnum PKGNUM
696 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
697 #with matching pkgnum.
701 #sub cust_credited_pkgnum {
702 # my( $self, $pkgnum ) = @_;
703 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
704 # sort { $a->_date <=> $b->_date }
705 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
706 # 'pkgnum' => $pkgnum,
711 #sub cust_credit_bill_pkgnum {
712 # shift->cust_credited_pkgnum(@_);
715 =item cust_credit_bill_pkg PKGNUM
717 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
718 applied against the matching pkgnum.
722 sub cust_credit_bill_pkg {
723 my( $self, $pkgnum ) = @_;
726 'select' => 'cust_credit_bill_pkg.*',
727 'table' => 'cust_credit_bill_pkg',
728 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
729 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
730 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
731 " AND cust_bill_pkg.pkgnum = $pkgnum",
738 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
745 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
747 foreach (@taxlines) { $total += $_->setup; }
753 Returns the amount owed (still outstanding) on this invoice, which is charged
754 minus all payment applications (see L<FS::cust_bill_pay>) and credit
755 applications (see L<FS::cust_credit_bill>).
761 my $balance = $self->charged;
762 $balance -= $_->amount foreach ( $self->cust_bill_pay );
763 $balance -= $_->amount foreach ( $self->cust_credited );
764 $balance = sprintf( "%.2f", $balance);
765 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
770 my( $self, $pkgnum ) = @_;
772 #my $balance = $self->charged;
774 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
776 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
777 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
779 $balance = sprintf( "%.2f", $balance);
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
784 =item apply_payments_and_credits [ OPTION => VALUE ... ]
786 Applies unapplied payments and credits to this invoice.
788 A hash of optional arguments may be passed. Currently "manual" is supported.
789 If true, a payment receipt is sent instead of a statement when
790 'payment_receipt_email' configuration option is set.
792 If there is an error, returns the error, otherwise returns false.
796 sub apply_payments_and_credits {
797 my( $self, %options ) = @_;
799 local $SIG{HUP} = 'IGNORE';
800 local $SIG{INT} = 'IGNORE';
801 local $SIG{QUIT} = 'IGNORE';
802 local $SIG{TERM} = 'IGNORE';
803 local $SIG{TSTP} = 'IGNORE';
804 local $SIG{PIPE} = 'IGNORE';
806 my $oldAutoCommit = $FS::UID::AutoCommit;
807 local $FS::UID::AutoCommit = 0;
810 $self->select_for_update; #mutex
812 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
813 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
815 if ( $conf->exists('pkg-balances') ) {
816 # limit @payments & @credits to those w/ a pkgnum grepped from $self
817 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
818 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
819 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
822 while ( $self->owed > 0 and ( @payments || @credits ) ) {
825 if ( @payments && @credits ) {
827 #decide which goes first by weight of top (unapplied) line item
829 my @open_lineitems = $self->open_cust_bill_pkg;
832 max( map { $_->part_pkg->pay_weight || 0 }
837 my $max_credit_weight =
838 max( map { $_->part_pkg->credit_weight || 0 }
844 #if both are the same... payments first? it has to be something
845 if ( $max_pay_weight >= $max_credit_weight ) {
851 } elsif ( @payments ) {
853 } elsif ( @credits ) {
856 die "guru meditation #12 and 35";
860 if ( $app eq 'pay' ) {
862 my $payment = shift @payments;
863 $unapp_amount = $payment->unapplied;
864 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
865 $app->pkgnum( $payment->pkgnum )
866 if $conf->exists('pkg-balances') && $payment->pkgnum;
868 } elsif ( $app eq 'credit' ) {
870 my $credit = shift @credits;
871 $unapp_amount = $credit->credited;
872 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
873 $app->pkgnum( $credit->pkgnum )
874 if $conf->exists('pkg-balances') && $credit->pkgnum;
877 die "guru meditation #12 and 35";
881 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
882 warn "owed_pkgnum ". $app->pkgnum;
883 $owed = $self->owed_pkgnum($app->pkgnum);
887 next unless $owed > 0;
889 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
890 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
892 $app->invnum( $self->invnum );
894 my $error = $app->insert(%options);
896 $dbh->rollback if $oldAutoCommit;
897 return "Error inserting ". $app->table. " record: $error";
899 die $error if $error;
903 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
908 =item generate_email OPTION => VALUE ...
916 sender address, required
920 alternate template name, optional
924 text attachment arrayref, optional
928 email subject, optional
932 notice name instead of "Invoice", optional
936 Returns an argument list to be passed to L<FS::Misc::send_email>.
947 my $me = '[FS::cust_bill::generate_email]';
950 'from' => $args{'from'},
951 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
955 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
956 'template' => $args{'template'},
957 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
958 'no_coupon' => $args{'no_coupon'},
961 my $cust_main = $self->cust_main;
963 if (ref($args{'to'}) eq 'ARRAY') {
964 $return{'to'} = $args{'to'};
966 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
967 $cust_main->invoicing_list
971 if ( $conf->exists('invoice_html') ) {
973 warn "$me creating HTML/text multipart message"
976 $return{'nobody'} = 1;
978 my $alternative = build MIME::Entity
979 'Type' => 'multipart/alternative',
980 'Encoding' => '7bit',
981 'Disposition' => 'inline'
985 if ( $conf->exists('invoice_email_pdf')
986 and scalar($conf->config('invoice_email_pdf_note')) ) {
988 warn "$me using 'invoice_email_pdf_note' in multipart message"
990 $data = [ map { $_ . "\n" }
991 $conf->config('invoice_email_pdf_note')
996 warn "$me not using 'invoice_email_pdf_note' in multipart message"
998 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
999 $data = $args{'print_text'};
1001 $data = [ $self->print_text(\%opt) ];
1006 $alternative->attach(
1007 'Type' => 'text/plain',
1008 #'Encoding' => 'quoted-printable',
1009 'Encoding' => '7bit',
1011 'Disposition' => 'inline',
1014 $args{'from'} =~ /\@([\w\.\-]+)/;
1015 my $from = $1 || 'example.com';
1016 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1019 my $agentnum = $cust_main->agentnum;
1020 if ( defined($args{'template'}) && length($args{'template'})
1021 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1024 $logo = 'logo_'. $args{'template'}. '.png';
1028 my $image_data = $conf->config_binary( $logo, $agentnum);
1030 my $image = build MIME::Entity
1031 'Type' => 'image/png',
1032 'Encoding' => 'base64',
1033 'Data' => $image_data,
1034 'Filename' => 'logo.png',
1035 'Content-ID' => "<$content_id>",
1039 if($conf->exists('invoice-barcode')){
1040 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1041 $barcode = build MIME::Entity
1042 'Type' => 'image/png',
1043 'Encoding' => 'base64',
1044 'Data' => $self->invoice_barcode(0),
1045 'Filename' => 'barcode.png',
1046 'Content-ID' => "<$barcode_content_id>",
1048 $opt{'barcode_cid'} = $barcode_content_id;
1051 $alternative->attach(
1052 'Type' => 'text/html',
1053 'Encoding' => 'quoted-printable',
1054 'Data' => [ '<html>',
1057 ' '. encode_entities($return{'subject'}),
1060 ' <body bgcolor="#e8e8e8">',
1061 $self->print_html({ 'cid'=>$content_id, %opt }),
1065 'Disposition' => 'inline',
1066 #'Filename' => 'invoice.pdf',
1069 my @otherparts = ();
1070 if ( $cust_main->email_csv_cdr ) {
1072 push @otherparts, build MIME::Entity
1073 'Type' => 'text/csv',
1074 'Encoding' => '7bit',
1075 'Data' => [ map { "$_\n" }
1076 $self->call_details('prepend_billed_number' => 1)
1078 'Disposition' => 'attachment',
1079 'Filename' => 'usage-'. $self->invnum. '.csv',
1084 if ( $conf->exists('invoice_email_pdf') ) {
1089 # multipart/alternative
1095 my $related = build MIME::Entity 'Type' => 'multipart/related',
1096 'Encoding' => '7bit';
1098 #false laziness w/Misc::send_email
1099 $related->head->replace('Content-type',
1100 $related->mime_type.
1101 '; boundary="'. $related->head->multipart_boundary. '"'.
1102 '; type=multipart/alternative'
1105 $related->add_part($alternative);
1107 $related->add_part($image);
1109 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1111 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1115 #no other attachment:
1117 # multipart/alternative
1122 $return{'content-type'} = 'multipart/related';
1123 if($conf->exists('invoice-barcode')){
1124 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1127 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1129 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1130 #$return{'disposition'} = 'inline';
1136 if ( $conf->exists('invoice_email_pdf') ) {
1137 warn "$me creating PDF attachment"
1140 #mime parts arguments a la MIME::Entity->build().
1141 $return{'mimeparts'} = [
1142 { $self->mimebuild_pdf(\%opt) }
1146 if ( $conf->exists('invoice_email_pdf')
1147 and scalar($conf->config('invoice_email_pdf_note')) ) {
1149 warn "$me using 'invoice_email_pdf_note'"
1151 $return{'body'} = [ map { $_ . "\n" }
1152 $conf->config('invoice_email_pdf_note')
1157 warn "$me not using 'invoice_email_pdf_note'"
1159 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1160 $return{'body'} = $args{'print_text'};
1162 $return{'body'} = [ $self->print_text(\%opt) ];
1175 Returns a list suitable for passing to MIME::Entity->build(), representing
1176 this invoice as PDF attachment.
1183 'Type' => 'application/pdf',
1184 'Encoding' => 'base64',
1185 'Data' => [ $self->print_pdf(@_) ],
1186 'Disposition' => 'attachment',
1187 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1191 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1193 Sends this invoice to the destinations configured for this customer: sends
1194 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1196 Options can be passed as a hashref (recommended) or as a list of up to
1197 four values for templatename, agentnum, invoice_from and amount.
1199 I<template>, if specified, is the name of a suffix for alternate invoices.
1201 I<agentnum>, if specified, means that this invoice will only be sent for customers
1202 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1203 single agent) or an arrayref of agentnums.
1205 I<invoice_from>, if specified, overrides the default email invoice From: address.
1207 I<amount>, if specified, only sends the invoice if the total amount owed on this
1208 invoice and all older invoices is greater than the specified amount.
1210 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1214 sub queueable_send {
1217 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1218 or die "invalid invoice number: " . $opt{invnum};
1220 my @args = ( $opt{template}, $opt{agentnum} );
1221 push @args, $opt{invoice_from}
1222 if exists($opt{invoice_from}) && $opt{invoice_from};
1224 my $error = $self->send( @args );
1225 die $error if $error;
1232 my( $template, $invoice_from, $notice_name );
1234 my $balance_over = 0;
1238 $template = $opt->{'template'} || '';
1239 if ( $agentnums = $opt->{'agentnum'} ) {
1240 $agentnums = [ $agentnums ] unless ref($agentnums);
1242 $invoice_from = $opt->{'invoice_from'};
1243 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1244 $notice_name = $opt->{'notice_name'};
1246 $template = scalar(@_) ? shift : '';
1247 if ( scalar(@_) && $_[0] ) {
1248 $agentnums = ref($_[0]) ? shift : [ shift ];
1250 $invoice_from = shift if scalar(@_);
1251 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1254 return 'N/A' unless ! $agentnums
1255 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1258 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1260 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1261 $conf->config('invoice_from', $self->cust_main->agentnum );
1264 'template' => $template,
1265 'invoice_from' => $invoice_from,
1266 'notice_name' => ( $notice_name || 'Invoice' ),
1269 my @invoicing_list = $self->cust_main->invoicing_list;
1271 #$self->email_invoice(\%opt)
1273 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1275 #$self->print_invoice(\%opt)
1277 if grep { $_ eq 'POST' } @invoicing_list; #postal
1279 $self->fax_invoice(\%opt)
1280 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1286 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1288 Emails this invoice.
1290 Options can be passed as a hashref (recommended) or as a list of up to
1291 two values for templatename and invoice_from.
1293 I<template>, if specified, is the name of a suffix for alternate invoices.
1295 I<invoice_from>, if specified, overrides the default email invoice From: address.
1297 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1301 sub queueable_email {
1304 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1305 or die "invalid invoice number: " . $opt{invnum};
1307 my %args = ( 'template' => $opt{template} );
1308 $args{$_} = $opt{$_}
1309 foreach grep { exists($opt{$_}) && $opt{$_} }
1310 qw( invoice_from notice_name no_coupon );
1312 my $error = $self->email( \%args );
1313 die $error if $error;
1317 #sub email_invoice {
1321 my( $template, $invoice_from, $notice_name, $no_coupon );
1324 $template = $opt->{'template'} || '';
1325 $invoice_from = $opt->{'invoice_from'};
1326 $notice_name = $opt->{'notice_name'} || 'Invoice';
1327 $no_coupon = $opt->{'no_coupon'} || 0;
1329 $template = scalar(@_) ? shift : '';
1330 $invoice_from = shift if scalar(@_);
1331 $notice_name = 'Invoice';
1335 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1336 $conf->config('invoice_from', $self->cust_main->agentnum );
1338 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1339 $self->cust_main->invoicing_list;
1341 if ( ! @invoicing_list ) { #no recipients
1342 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1343 die 'No recipients for customer #'. $self->custnum;
1345 #default: better to notify this person than silence
1346 @invoicing_list = ($invoice_from);
1350 my $subject = $self->email_subject($template);
1352 my $error = send_email(
1353 $self->generate_email(
1354 'from' => $invoice_from,
1355 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1356 'subject' => $subject,
1357 'template' => $template,
1358 'notice_name' => $notice_name,
1359 'no_coupon' => $no_coupon,
1362 die "can't email invoice: $error\n" if $error;
1363 #die "$error\n" if $error;
1370 #my $template = scalar(@_) ? shift : '';
1373 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1376 my $cust_main = $self->cust_main;
1377 my $name = $cust_main->name;
1378 my $name_short = $cust_main->name_short;
1379 my $invoice_number = $self->invnum;
1380 my $invoice_date = $self->_date_pretty;
1382 eval qq("$subject");
1385 =item lpr_data HASHREF | [ TEMPLATE ]
1387 Returns the postscript or plaintext for this invoice as an arrayref.
1389 Options can be passed as a hashref (recommended) or as a single optional value
1392 I<template>, if specified, is the name of a suffix for alternate invoices.
1394 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1400 my( $template, $notice_name );
1403 $template = $opt->{'template'} || '';
1404 $notice_name = $opt->{'notice_name'} || 'Invoice';
1406 $template = scalar(@_) ? shift : '';
1407 $notice_name = 'Invoice';
1411 'template' => $template,
1412 'notice_name' => $notice_name,
1415 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1416 [ $self->$method( \%opt ) ];
1419 =item print HASHREF | [ TEMPLATE ]
1421 Prints this invoice.
1423 Options can be passed as a hashref (recommended) or as a single optional
1426 I<template>, if specified, is the name of a suffix for alternate invoices.
1428 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1432 #sub print_invoice {
1435 my( $template, $notice_name );
1438 $template = $opt->{'template'} || '';
1439 $notice_name = $opt->{'notice_name'} || 'Invoice';
1441 $template = scalar(@_) ? shift : '';
1442 $notice_name = 'Invoice';
1446 'template' => $template,
1447 'notice_name' => $notice_name,
1450 if($conf->exists('invoice_print_pdf')) {
1451 # Add the invoice to the current batch.
1452 $self->batch_invoice(\%opt);
1455 do_print $self->lpr_data(\%opt);
1459 =item fax_invoice HASHREF | [ TEMPLATE ]
1463 Options can be passed as a hashref (recommended) or as a single optional
1466 I<template>, if specified, is the name of a suffix for alternate invoices.
1468 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1474 my( $template, $notice_name );
1477 $template = $opt->{'template'} || '';
1478 $notice_name = $opt->{'notice_name'} || 'Invoice';
1480 $template = scalar(@_) ? shift : '';
1481 $notice_name = 'Invoice';
1484 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1485 unless $conf->exists('invoice_latex');
1487 my $dialstring = $self->cust_main->getfield('fax');
1491 'template' => $template,
1492 'notice_name' => $notice_name,
1495 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1496 'dialstring' => $dialstring,
1498 die $error if $error;
1502 =item batch_invoice [ HASHREF ]
1504 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1505 isn't an open batch, one will be created.
1510 my ($self, $opt) = @_;
1511 my $batch = FS::bill_batch->get_open_batch;
1512 my $cust_bill_batch = FS::cust_bill_batch->new({
1513 batchnum => $batch->batchnum,
1514 invnum => $self->invnum,
1516 return $cust_bill_batch->insert($opt);
1519 =item ftp_invoice [ TEMPLATENAME ]
1521 Sends this invoice data via FTP.
1523 TEMPLATENAME is unused?
1529 my $template = scalar(@_) ? shift : '';
1532 'protocol' => 'ftp',
1533 'server' => $conf->config('cust_bill-ftpserver'),
1534 'username' => $conf->config('cust_bill-ftpusername'),
1535 'password' => $conf->config('cust_bill-ftppassword'),
1536 'dir' => $conf->config('cust_bill-ftpdir'),
1537 'format' => $conf->config('cust_bill-ftpformat'),
1541 =item spool_invoice [ TEMPLATENAME ]
1543 Spools this invoice data (see L<FS::spool_csv>)
1545 TEMPLATENAME is unused?
1551 my $template = scalar(@_) ? shift : '';
1554 'format' => $conf->config('cust_bill-spoolformat'),
1555 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1559 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1561 Like B<send>, but only sends the invoice if it is the newest open invoice for
1566 sub send_if_newest {
1571 grep { $_->owed > 0 }
1572 qsearch('cust_bill', {
1573 'custnum' => $self->custnum,
1574 #'_date' => { op=>'>', value=>$self->_date },
1575 'invnum' => { op=>'>', value=>$self->invnum },
1582 =item send_csv OPTION => VALUE, ...
1584 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1588 protocol - currently only "ftp"
1594 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1595 and YYMMDDHHMMSS is a timestamp.
1597 See L</print_csv> for a description of the output format.
1602 my($self, %opt) = @_;
1606 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1607 mkdir $spooldir, 0700 unless -d $spooldir;
1609 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1610 my $file = "$spooldir/$tracctnum.csv";
1612 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1614 open(CSV, ">$file") or die "can't open $file: $!";
1622 if ( $opt{protocol} eq 'ftp' ) {
1623 eval "use Net::FTP;";
1625 $net = Net::FTP->new($opt{server}) or die @$;
1627 die "unknown protocol: $opt{protocol}";
1630 $net->login( $opt{username}, $opt{password} )
1631 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1633 $net->binary or die "can't set binary mode";
1635 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1637 $net->put($file) or die "can't put $file: $!";
1647 Spools CSV invoice data.
1653 =item format - 'default' or 'billco'
1655 =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>).
1657 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1659 =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.
1666 my($self, %opt) = @_;
1668 my $cust_main = $self->cust_main;
1670 if ( $opt{'dest'} ) {
1671 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1672 $cust_main->invoicing_list;
1673 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1674 || ! keys %invoicing_list;
1677 if ( $opt{'balanceover'} ) {
1679 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1682 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1683 mkdir $spooldir, 0700 unless -d $spooldir;
1685 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1689 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1690 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1693 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1695 open(CSV, ">>$file") or die "can't open $file: $!";
1696 flock(CSV, LOCK_EX);
1701 if ( lc($opt{'format'}) eq 'billco' ) {
1703 flock(CSV, LOCK_UN);
1708 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1711 open(CSV,">>$file") or die "can't open $file: $!";
1712 flock(CSV, LOCK_EX);
1718 flock(CSV, LOCK_UN);
1725 =item print_csv OPTION => VALUE, ...
1727 Returns CSV data for this invoice.
1731 format - 'default' or 'billco'
1733 Returns a list consisting of two scalars. The first is a single line of CSV
1734 header information for this invoice. The second is one or more lines of CSV
1735 detail information for this invoice.
1737 If I<format> is not specified or "default", the fields of the CSV file are as
1740 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1744 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1746 B<record_type> is C<cust_bill> for the initial header line only. The
1747 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1748 fields are filled in.
1750 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1751 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1754 =item invnum - invoice number
1756 =item custnum - customer number
1758 =item _date - invoice date
1760 =item charged - total invoice amount
1762 =item first - customer first name
1764 =item last - customer first name
1766 =item company - company name
1768 =item address1 - address line 1
1770 =item address2 - address line 1
1780 =item pkg - line item description
1782 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1784 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1786 =item sdate - start date for recurring fee
1788 =item edate - end date for recurring fee
1792 If I<format> is "billco", the fields of the header CSV file are as follows:
1794 +-------------------------------------------------------------------+
1795 | FORMAT HEADER FILE |
1796 |-------------------------------------------------------------------|
1797 | Field | Description | Name | Type | Width |
1798 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1799 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1800 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1801 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1802 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1803 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1804 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1805 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1806 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1807 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1808 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1809 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1810 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1811 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1812 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1813 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1814 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1815 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1816 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1817 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1818 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1819 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1820 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1821 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1822 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1823 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1824 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1825 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1826 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1827 +-------+-------------------------------+------------+------+-------+
1829 If I<format> is "billco", the fields of the detail CSV file are as follows:
1831 FORMAT FOR DETAIL FILE
1833 Field | Description | Name | Type | Width
1834 1 | N/A-Leave Empty | RC | CHAR | 2
1835 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1836 3 | Account Number | TRACCTNUM | CHAR | 15
1837 4 | Invoice Number | TRINVOICE | CHAR | 15
1838 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1839 6 | Transaction Detail | DETAILS | CHAR | 100
1840 7 | Amount | AMT | NUM* | 9
1841 8 | Line Format Control** | LNCTRL | CHAR | 2
1842 9 | Grouping Code | GROUP | CHAR | 2
1843 10 | User Defined | ACCT CODE | CHAR | 15
1848 my($self, %opt) = @_;
1850 eval "use Text::CSV_XS";
1853 my $cust_main = $self->cust_main;
1855 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1857 if ( lc($opt{'format'}) eq 'billco' ) {
1860 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1862 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1864 my( $previous_balance, @unused ) = $self->previous; #previous balance
1866 my $pmt_cr_applied = 0;
1867 $pmt_cr_applied += $_->{'amount'}
1868 foreach ( $self->_items_payments, $self->_items_credits ) ;
1870 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1873 '', # 1 | N/A-Leave Empty CHAR 2
1874 '', # 2 | N/A-Leave Empty CHAR 15
1875 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1876 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1877 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1878 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1879 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1880 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1881 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1882 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1883 '', # 10 | Ancillary Billing Information CHAR 30
1884 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1885 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1888 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1891 $duedate, # 14 | Bill Due Date CHAR 10
1893 $previous_balance, # 15 | Previous Balance NUM* 9
1894 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1895 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1896 $totaldue, # 18 | Total Amt Due NUM* 9
1897 $totaldue, # 19 | Total Amt Due NUM* 9
1898 '', # 20 | 30 Day Aging NUM* 9
1899 '', # 21 | 60 Day Aging NUM* 9
1900 '', # 22 | 90 Day Aging NUM* 9
1901 'N', # 23 | Y/N CHAR 1
1902 '', # 24 | Remittance automation CHAR 100
1903 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1904 $self->custnum, # 26 | Customer Reference Number CHAR 15
1905 '0', # 27 | Federal Tax*** NUM* 9
1906 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1907 '0', # 29 | Other Taxes & Fees*** NUM* 9
1916 time2str("%x", $self->_date),
1917 sprintf("%.2f", $self->charged),
1918 ( map { $cust_main->getfield($_) }
1919 qw( first last company address1 address2 city state zip country ) ),
1921 ) or die "can't create csv";
1924 my $header = $csv->string. "\n";
1927 if ( lc($opt{'format'}) eq 'billco' ) {
1930 foreach my $item ( $self->_items_pkg ) {
1933 '', # 1 | N/A-Leave Empty CHAR 2
1934 '', # 2 | N/A-Leave Empty CHAR 15
1935 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1936 $self->invnum, # 4 | Invoice Number CHAR 15
1937 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1938 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1939 $item->{'amount'}, # 7 | Amount NUM* 9
1940 '', # 8 | Line Format Control** CHAR 2
1941 '', # 9 | Grouping Code CHAR 2
1942 '', # 10 | User Defined CHAR 15
1945 $detail .= $csv->string. "\n";
1951 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1953 my($pkg, $setup, $recur, $sdate, $edate);
1954 if ( $cust_bill_pkg->pkgnum ) {
1956 ($pkg, $setup, $recur, $sdate, $edate) = (
1957 $cust_bill_pkg->part_pkg->pkg,
1958 ( $cust_bill_pkg->setup != 0
1959 ? sprintf("%.2f", $cust_bill_pkg->setup )
1961 ( $cust_bill_pkg->recur != 0
1962 ? sprintf("%.2f", $cust_bill_pkg->recur )
1964 ( $cust_bill_pkg->sdate
1965 ? time2str("%x", $cust_bill_pkg->sdate)
1967 ($cust_bill_pkg->edate
1968 ?time2str("%x", $cust_bill_pkg->edate)
1972 } else { #pkgnum tax
1973 next unless $cust_bill_pkg->setup != 0;
1974 $pkg = $cust_bill_pkg->desc;
1975 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1976 ( $sdate, $edate ) = ( '', '' );
1982 ( map { '' } (1..11) ),
1983 ($pkg, $setup, $recur, $sdate, $edate)
1984 ) or die "can't create csv";
1986 $detail .= $csv->string. "\n";
1992 ( $header, $detail );
1998 Pays this invoice with a compliemntary payment. If there is an error,
1999 returns the error, otherwise returns false.
2005 my $cust_pay = new FS::cust_pay ( {
2006 'invnum' => $self->invnum,
2007 'paid' => $self->owed,
2010 'payinfo' => $self->cust_main->payinfo,
2018 Attempts to pay this invoice with a credit card payment via a
2019 Business::OnlinePayment realtime gateway. See
2020 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2021 for supported processors.
2027 $self->realtime_bop( 'CC', @_ );
2032 Attempts to pay this invoice with an electronic check (ACH) payment via a
2033 Business::OnlinePayment realtime gateway. See
2034 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2035 for supported processors.
2041 $self->realtime_bop( 'ECHECK', @_ );
2046 Attempts to pay this invoice with phone bill (LEC) payment via a
2047 Business::OnlinePayment realtime gateway. See
2048 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2049 for supported processors.
2055 $self->realtime_bop( 'LEC', @_ );
2059 my( $self, $method ) = (shift,shift);
2062 my $cust_main = $self->cust_main;
2063 my $balance = $cust_main->balance;
2064 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2065 $amount = sprintf("%.2f", $amount);
2066 return "not run (balance $balance)" unless $amount > 0;
2068 my $description = 'Internet Services';
2069 if ( $conf->exists('business-onlinepayment-description') ) {
2070 my $dtempl = $conf->config('business-onlinepayment-description');
2072 my $agent_obj = $cust_main->agent
2073 or die "can't retreive agent for $cust_main (agentnum ".
2074 $cust_main->agentnum. ")";
2075 my $agent = $agent_obj->agent;
2076 my $pkgs = join(', ',
2077 map { $_->part_pkg->pkg }
2078 grep { $_->pkgnum } $self->cust_bill_pkg
2080 $description = eval qq("$dtempl");
2083 $cust_main->realtime_bop($method, $amount,
2084 'description' => $description,
2085 'invnum' => $self->invnum,
2086 #this didn't do what we want, it just calls apply_payments_and_credits
2088 'apply_to_invoice' => 1,
2091 #this changes application behavior: auto payments
2092 #triggered against a specific invoice are now applied
2093 #to that invoice instead of oldest open.
2099 =item batch_card OPTION => VALUE...
2101 Adds a payment for this invoice to the pending credit card batch (see
2102 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2103 runs the payment using a realtime gateway.
2108 my ($self, %options) = @_;
2109 my $cust_main = $self->cust_main;
2111 $options{invnum} = $self->invnum;
2113 $cust_main->batch_card(%options);
2116 sub _agent_template {
2118 $self->cust_main->agent_template;
2121 sub _agent_invoice_from {
2123 $self->cust_main->agent_invoice_from;
2126 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2128 Returns an text invoice, as a list of lines.
2130 Options can be passed as a hashref (recommended) or as a list of time, template
2131 and then any key/value pairs for any other options.
2133 I<time>, if specified, is used to control the printing of overdue messages. The
2134 default is now. It isn't the date of the invoice; that's the `_date' field.
2135 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2136 L<Time::Local> and L<Date::Parse> for conversion functions.
2138 I<template>, if specified, is the name of a suffix for alternate invoices.
2140 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2146 my( $today, $template, %opt );
2148 %opt = %{ shift() };
2149 $today = delete($opt{'time'}) || '';
2150 $template = delete($opt{template}) || '';
2152 ( $today, $template, %opt ) = @_;
2155 my %params = ( 'format' => 'template' );
2156 $params{'time'} = $today if $today;
2157 $params{'template'} = $template if $template;
2158 $params{$_} = $opt{$_}
2159 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2161 $self->print_generic( %params );
2164 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2166 Internal method - returns a filename of a filled-in LaTeX template for this
2167 invoice (Note: add ".tex" to get the actual filename), and a filename of
2168 an associated logo (with the .eps extension included).
2170 See print_ps and print_pdf for methods that return PostScript and PDF output.
2172 Options can be passed as a hashref (recommended) or as a list of time, template
2173 and then any key/value pairs for any other options.
2175 I<time>, if specified, is used to control the printing of overdue messages. The
2176 default is now. It isn't the date of the invoice; that's the `_date' field.
2177 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2178 L<Time::Local> and L<Date::Parse> for conversion functions.
2180 I<template>, if specified, is the name of a suffix for alternate invoices.
2182 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2188 my( $today, $template, %opt );
2190 %opt = %{ shift() };
2191 $today = delete($opt{'time'}) || '';
2192 $template = delete($opt{template}) || '';
2194 ( $today, $template, %opt ) = @_;
2197 my %params = ( 'format' => 'latex' );
2198 $params{'time'} = $today if $today;
2199 $params{'template'} = $template if $template;
2200 $params{$_} = $opt{$_}
2201 foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2203 $template ||= $self->_agent_template;
2205 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2206 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2210 ) or die "can't open temp file: $!\n";
2212 my $agentnum = $self->cust_main->agentnum;
2214 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2215 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2216 or die "can't write temp file: $!\n";
2218 print $lh $conf->config_binary('logo.eps', $agentnum)
2219 or die "can't write temp file: $!\n";
2222 $params{'logo_file'} = $lh->filename;
2224 if($conf->exists('invoice-barcode')){
2225 my $png_file = $self->invoice_barcode($dir);
2226 my $eps_file = $png_file;
2227 $eps_file =~ s/\.png$/.eps/g;
2228 $png_file =~ /(barcode.*png)/;
2230 $eps_file =~ /(barcode.*eps)/;
2233 my $curr_dir = cwd();
2235 # after painfuly long experimentation, it was determined that sam2p won't
2236 # accept : and other chars in the path, no matter how hard I tried to
2237 # escape them, hence the chdir (and chdir back, just to be safe)
2238 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2239 or die "sam2p failed: $!\n";
2243 $params{'barcode_file'} = $eps_file;
2246 my @filled_in = $self->print_generic( %params );
2248 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2252 ) or die "can't open temp file: $!\n";
2253 print $fh join('', @filled_in );
2256 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2257 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2261 =item invoice_barcode DIR_OR_FALSE
2263 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2264 it is taken as the temp directory where the PNG file will be generated and the
2265 PNG file name is returned. Otherwise, the PNG image itself is returned.
2269 sub invoice_barcode {
2270 my ($self, $dir) = (shift,shift);
2272 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2273 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2274 my $gd = $gdbar->plot(Height => 30);
2277 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2281 ) or die "can't open temp file: $!\n";
2282 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2283 my $png_file = $bh->filename;
2290 =item print_generic OPTION => VALUE ...
2292 Internal method - returns a filled-in template for this invoice as a scalar.
2294 See print_ps and print_pdf for methods that return PostScript and PDF output.
2296 Non optional options include
2297 format - latex, html, template
2299 Optional options include
2301 template - a value used as a suffix for a configuration template
2303 time - a value used to control the printing of overdue messages. The
2304 default is now. It isn't the date of the invoice; that's the `_date' field.
2305 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2306 L<Time::Local> and L<Date::Parse> for conversion functions.
2310 unsquelch_cdr - overrides any per customer cdr squelching when true
2312 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2316 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2317 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2318 # yes: fixed width (dot matrix) text printing will be borked
2321 my( $self, %params ) = @_;
2322 my $today = $params{today} ? $params{today} : time;
2323 warn "$me print_generic called on $self with suffix $params{template}\n"
2326 my $format = $params{format};
2327 die "Unknown format: $format"
2328 unless $format =~ /^(latex|html|template)$/;
2330 my $cust_main = $self->cust_main;
2331 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2332 unless $cust_main->payname
2333 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2335 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2336 'html' => [ '<%=', '%>' ],
2337 'template' => [ '{', '}' ],
2340 warn "$me print_generic creating template\n"
2343 #create the template
2344 my $template = $params{template} ? $params{template} : $self->_agent_template;
2345 my $templatefile = "invoice_$format";
2346 $templatefile .= "_$template"
2347 if length($template) && $conf->exists($templatefile."_$template");
2348 my @invoice_template = map "$_\n", $conf->config($templatefile)
2349 or die "cannot load config data $templatefile";
2352 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2353 #change this to a die when the old code is removed
2354 warn "old-style invoice template $templatefile; ".
2355 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2356 $old_latex = 'true';
2357 @invoice_template = _translate_old_latex_format(@invoice_template);
2360 warn "$me print_generic creating T:T object\n"
2363 my $text_template = new Text::Template(
2365 SOURCE => \@invoice_template,
2366 DELIMITERS => $delimiters{$format},
2369 warn "$me print_generic compiling T:T object\n"
2372 $text_template->compile()
2373 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2376 # additional substitution could possibly cause breakage in existing templates
2377 my %convert_maps = (
2379 'notes' => sub { map "$_", @_ },
2380 'footer' => sub { map "$_", @_ },
2381 'smallfooter' => sub { map "$_", @_ },
2382 'returnaddress' => sub { map "$_", @_ },
2383 'coupon' => sub { map "$_", @_ },
2384 'summary' => sub { map "$_", @_ },
2390 s/%%(.*)$/<!-- $1 -->/g;
2391 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2392 s/\\begin\{enumerate\}/<ol>/g;
2394 s/\\end\{enumerate\}/<\/ol>/g;
2395 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2404 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2406 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2411 s/\\\\\*?\s*$/<BR>/;
2412 s/\\hyphenation\{[\w\s\-]+}//;
2417 'coupon' => sub { "" },
2418 'summary' => sub { "" },
2425 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2426 s/\\begin\{enumerate\}//g;
2428 s/\\end\{enumerate\}//g;
2429 s/\\textbf\{(.*)\}/$1/g;
2436 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2438 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2443 s/\\\\\*?\s*$/\n/; # dubious
2444 s/\\hyphenation\{[\w\s\-]+}//;
2448 'coupon' => sub { "" },
2449 'summary' => sub { "" },
2454 # hashes for differing output formats
2455 my %nbsps = ( 'latex' => '~',
2456 'html' => '', # '&nbps;' would be nice
2457 'template' => '', # not used
2459 my $nbsp = $nbsps{$format};
2461 my %escape_functions = ( 'latex' => \&_latex_escape,
2462 'html' => \&_html_escape_nbsp,#\&encode_entities,
2463 'template' => sub { shift },
2465 my $escape_function = $escape_functions{$format};
2466 my $escape_function_nonbsp = ($format eq 'html')
2467 ? \&_html_escape : $escape_function;
2469 my %date_formats = ( 'latex' => $date_format_long,
2470 'html' => $date_format_long,
2473 $date_formats{'html'} =~ s/ / /g;
2475 my $date_format = $date_formats{$format};
2477 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2479 'html' => sub { return '<b>'. shift(). '</b>'
2481 'template' => sub { shift },
2483 my $embolden_function = $embolden_functions{$format};
2485 my %newline_tokens = ( 'latex' => '\\\\',
2489 my $newline_token = $newline_tokens{$format};
2491 warn "$me generating template variables\n"
2494 # generate template variables
2497 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2501 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2507 $returnaddress = join("\n",
2508 $conf->config_orbase("invoice_${format}returnaddress", $template)
2511 } elsif ( grep /\S/,
2512 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2514 my $convert_map = $convert_maps{$format}{'returnaddress'};
2517 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2522 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2524 my $convert_map = $convert_maps{$format}{'returnaddress'};
2525 $returnaddress = join( "\n", &$convert_map(
2526 map { s/( {2,})/'~' x length($1)/eg;
2530 ( $conf->config('company_name', $self->cust_main->agentnum),
2531 $conf->config('company_address', $self->cust_main->agentnum),
2538 my $warning = "Couldn't find a return address; ".
2539 "do you need to set the company_address configuration value?";
2541 $returnaddress = $nbsp;
2542 #$returnaddress = $warning;
2546 warn "$me generating invoice data\n"
2549 my $agentnum = $self->cust_main->agentnum;
2551 my %invoice_data = (
2554 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2555 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2556 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2557 'returnaddress' => $returnaddress,
2558 'agent' => &$escape_function($cust_main->agent->agent),
2561 'invnum' => $self->invnum,
2562 'date' => time2str($date_format, $self->_date),
2563 'today' => time2str($date_format_long, $today),
2564 'terms' => $self->terms,
2565 'template' => $template, #params{'template'},
2566 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2567 'current_charges' => sprintf("%.2f", $self->charged),
2568 'duedate' => $self->due_date2str($rdate_format), #date_format?
2571 'custnum' => $cust_main->display_custnum,
2572 'agent_custid' => &$escape_function($cust_main->agent_custid),
2573 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2574 payname company address1 address2 city state zip fax
2578 'ship_enable' => $conf->exists('invoice-ship_address'),
2579 'unitprices' => $conf->exists('invoice-unitprice'),
2580 'smallernotes' => $conf->exists('invoice-smallernotes'),
2581 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2582 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2584 #layout info -- would be fancy to calc some of this and bury the template
2586 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2587 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2588 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2589 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2590 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2591 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2592 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2593 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2594 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2595 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2597 # better hang on to conf_dir for a while (for old templates)
2598 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2600 #these are only used when doing paged plaintext
2606 my $min_sdate = 999999999999;
2608 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2609 next unless $cust_bill_pkg->pkgnum > 0;
2610 $min_sdate = $cust_bill_pkg->sdate
2611 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2612 $max_edate = $cust_bill_pkg->edate
2613 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2616 $invoice_data{'bill_period'} = '';
2617 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2618 . " to " . time2str('%e %h', $max_edate)
2619 if ($max_edate != 0 && $min_sdate != 999999999999);
2621 $invoice_data{finance_section} = '';
2622 if ( $conf->config('finance_pkgclass') ) {
2624 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2625 $invoice_data{finance_section} = $pkg_class->categoryname;
2627 $invoice_data{finance_amount} = '0.00';
2628 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2630 my $countrydefault = $conf->config('countrydefault') || 'US';
2631 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2632 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2633 my $method = $prefix.$_;
2634 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2636 $invoice_data{'ship_country'} = ''
2637 if ( $invoice_data{'ship_country'} eq $countrydefault );
2639 $invoice_data{'cid'} = $params{'cid'}
2642 if ( $cust_main->country eq $countrydefault ) {
2643 $invoice_data{'country'} = '';
2645 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2649 $invoice_data{'address'} = \@address;
2651 $cust_main->payname.
2652 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2653 ? " (P.O. #". $cust_main->payinfo. ")"
2657 push @address, $cust_main->company
2658 if $cust_main->company;
2659 push @address, $cust_main->address1;
2660 push @address, $cust_main->address2
2661 if $cust_main->address2;
2663 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2664 push @address, $invoice_data{'country'}
2665 if $invoice_data{'country'};
2667 while (scalar(@address) < 5);
2669 $invoice_data{'logo_file'} = $params{'logo_file'}
2670 if $params{'logo_file'};
2671 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2672 if $params{'barcode_file'};
2673 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2674 if $params{'barcode_img'};
2675 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2676 if $params{'barcode_cid'};
2678 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2679 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2680 #my $balance_due = $self->owed + $pr_total - $cr_total;
2681 my $balance_due = $self->owed + $pr_total;
2682 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2683 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2684 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2685 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2687 my $summarypage = '';
2688 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2691 $invoice_data{'summarypage'} = $summarypage;
2693 warn "$me substituting variables in notes, footer, smallfooter\n"
2696 my @include = (qw( notes footer smallfooter ));
2697 push @include, 'coupon' unless $params{'no_coupon'};
2698 foreach my $include (@include) {
2700 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2703 if ( $conf->exists($inc_file, $agentnum)
2704 && length( $conf->config($inc_file, $agentnum) ) ) {
2706 @inc_src = $conf->config($inc_file, $agentnum);
2710 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2712 my $convert_map = $convert_maps{$format}{$include};
2714 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2715 s/--\@\]/$delimiters{$format}[1]/g;
2718 &$convert_map( $conf->config($inc_file, $agentnum) );
2722 my $inc_tt = new Text::Template (
2724 SOURCE => [ map "$_\n", @inc_src ],
2725 DELIMITERS => $delimiters{$format},
2726 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2728 unless ( $inc_tt->compile() ) {
2729 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2730 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2734 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2736 $invoice_data{$include} =~ s/\n+$//
2737 if ($format eq 'latex');
2740 $invoice_data{'po_line'} =
2741 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2742 ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2745 my %money_chars = ( 'latex' => '',
2746 'html' => $conf->config('money_char') || '$',
2749 my $money_char = $money_chars{$format};
2751 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2752 'html' => $conf->config('money_char') || '$',
2755 my $other_money_char = $other_money_chars{$format};
2756 $invoice_data{'dollar'} = $other_money_char;
2758 my @detail_items = ();
2759 my @total_items = ();
2763 $invoice_data{'detail_items'} = \@detail_items;
2764 $invoice_data{'total_items'} = \@total_items;
2765 $invoice_data{'buf'} = \@buf;
2766 $invoice_data{'sections'} = \@sections;
2768 warn "$me generating sections\n"
2771 my $previous_section = { 'description' => 'Previous Charges',
2772 'subtotal' => $other_money_char.
2773 sprintf('%.2f', $pr_total),
2774 'summarized' => $summarypage ? 'Y' : '',
2776 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2777 join(' / ', map { $cust_main->balance_date_range(@$_) }
2778 $self->_prior_month30s
2780 if $conf->exists('invoice_include_aging');
2783 my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2784 'subtotal' => $taxtotal, # adjusted below
2785 'summarized' => $summarypage ? 'Y' : '',
2787 my $tax_weight = _pkg_category($tax_section->{description})
2788 ? _pkg_category($tax_section->{description})->weight
2790 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2791 $tax_section->{'sort_weight'} = $tax_weight;
2794 my $adjusttotal = 0;
2795 my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2796 'subtotal' => 0, # adjusted below
2797 'summarized' => $summarypage ? 'Y' : '',
2799 my $adjust_weight = _pkg_category($adjust_section->{description})
2800 ? _pkg_category($adjust_section->{description})->weight
2802 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2803 $adjust_section->{'sort_weight'} = $adjust_weight;
2805 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2806 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2807 $invoice_data{'multisection'} = $multisection;
2808 my $late_sections = [];
2809 my $extra_sections = [];
2810 my $extra_lines = ();
2811 if ( $multisection ) {
2812 ($extra_sections, $extra_lines) =
2813 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2814 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2816 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2818 push @detail_items, @$extra_lines if $extra_lines;
2820 $self->_items_sections( $late_sections, # this could stand a refactor
2822 $escape_function_nonbsp,
2826 if ($conf->exists('svc_phone_sections')) {
2827 my ($phone_sections, $phone_lines) =
2828 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2829 push @{$late_sections}, @$phone_sections;
2830 push @detail_items, @$phone_lines;
2832 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2833 my ($accountcode_section, $accountcode_lines) =
2834 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2835 if ( scalar(@$accountcode_lines) ) {
2836 push @{$late_sections}, $accountcode_section;
2837 push @detail_items, @$accountcode_lines;
2841 push @sections, { 'description' => '', 'subtotal' => '' };
2844 unless ( $conf->exists('disable_previous_balance')
2845 || $conf->exists('previous_balance-summary_only')
2849 warn "$me adding previous balances\n"
2852 foreach my $line_item ( $self->_items_previous ) {
2855 ext_description => [],
2857 $detail->{'ref'} = $line_item->{'pkgnum'};
2858 $detail->{'quantity'} = 1;
2859 $detail->{'section'} = $previous_section;
2860 $detail->{'description'} = &$escape_function($line_item->{'description'});
2861 if ( exists $line_item->{'ext_description'} ) {
2862 @{$detail->{'ext_description'}} = map {
2863 &$escape_function($_);
2864 } @{$line_item->{'ext_description'}};
2866 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2867 $line_item->{'amount'};
2868 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2870 push @detail_items, $detail;
2871 push @buf, [ $detail->{'description'},
2872 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2878 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2879 push @buf, ['','-----------'];
2880 push @buf, [ 'Total Previous Balance',
2881 $money_char. sprintf("%10.2f", $pr_total) ];
2885 if ( $conf->exists('svc_phone-did-summary') ) {
2886 warn "$me adding DID summary\n"
2889 my ($didsummary,$minutes) = $self->_did_summary;
2890 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2892 { 'description' => $didsummary_desc,
2893 'ext_description' => [ $didsummary, $minutes ],
2898 foreach my $section (@sections, @$late_sections) {
2900 warn "$me adding section \n". Dumper($section)
2903 # begin some normalization
2904 $section->{'subtotal'} = $section->{'amount'}
2906 && !exists($section->{subtotal})
2907 && exists($section->{amount});
2909 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2910 if ( $invoice_data{finance_section} &&
2911 $section->{'description'} eq $invoice_data{finance_section} );
2913 $section->{'subtotal'} = $other_money_char.
2914 sprintf('%.2f', $section->{'subtotal'})
2917 # continue some normalization
2918 $section->{'amount'} = $section->{'subtotal'}
2922 if ( $section->{'description'} ) {
2923 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2928 warn "$me setting options\n"
2931 my $multilocation = scalar($cust_main->cust_location); #too expensive?
2933 $options{'section'} = $section if $multisection;
2934 $options{'format'} = $format;
2935 $options{'escape_function'} = $escape_function;
2936 $options{'format_function'} = sub { () } unless $unsquelched;
2937 $options{'unsquelched'} = $unsquelched;
2938 $options{'summary_page'} = $summarypage;
2939 $options{'skip_usage'} =
2940 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2941 $options{'multilocation'} = $multilocation;
2942 $options{'multisection'} = $multisection;
2944 warn "$me searching for line items\n"
2947 foreach my $line_item ( $self->_items_pkg(%options) ) {
2949 warn "$me adding line item $line_item\n"
2953 ext_description => [],
2955 $detail->{'ref'} = $line_item->{'pkgnum'};
2956 $detail->{'quantity'} = $line_item->{'quantity'};
2957 $detail->{'section'} = $section;
2958 $detail->{'description'} = &$escape_function($line_item->{'description'});
2959 if ( exists $line_item->{'ext_description'} ) {
2960 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2962 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2963 $line_item->{'amount'};
2964 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2965 $line_item->{'unit_amount'};
2966 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2968 push @detail_items, $detail;
2969 push @buf, ( [ $detail->{'description'},
2970 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2972 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2976 if ( $section->{'description'} ) {
2977 push @buf, ( ['','-----------'],
2978 [ $section->{'description'}. ' sub-total',
2979 $money_char. sprintf("%10.2f", $section->{'subtotal'})
2988 $invoice_data{current_less_finance} =
2989 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2991 if ( $multisection && !$conf->exists('disable_previous_balance')
2992 || $conf->exists('previous_balance-summary_only') )
2994 unshift @sections, $previous_section if $pr_total;
2997 warn "$me adding taxes\n"
3000 foreach my $tax ( $self->_items_tax ) {
3002 $taxtotal += $tax->{'amount'};
3004 my $description = &$escape_function( $tax->{'description'} );
3005 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3007 if ( $multisection ) {
3009 my $money = $old_latex ? '' : $money_char;
3010 push @detail_items, {
3011 ext_description => [],
3014 description => $description,
3015 amount => $money. $amount,
3017 section => $tax_section,
3022 push @total_items, {
3023 'total_item' => $description,
3024 'total_amount' => $other_money_char. $amount,
3029 push @buf,[ $description,
3030 $money_char. $amount,
3037 $total->{'total_item'} = 'Sub-total';
3038 $total->{'total_amount'} =
3039 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3041 if ( $multisection ) {
3042 $tax_section->{'subtotal'} = $other_money_char.
3043 sprintf('%.2f', $taxtotal);
3044 $tax_section->{'pretotal'} = 'New charges sub-total '.
3045 $total->{'total_amount'};
3046 push @sections, $tax_section if $taxtotal;
3048 unshift @total_items, $total;
3051 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3053 push @buf,['','-----------'];
3054 push @buf,[( $conf->exists('disable_previous_balance')
3056 : 'Total New Charges'
3058 $money_char. sprintf("%10.2f",$self->charged) ];
3064 $item = $conf->config('previous_balance-exclude_from_total')
3065 || 'Total New Charges'
3066 if $conf->exists('previous_balance-exclude_from_total');
3067 my $amount = $self->charged +
3068 ( $conf->exists('disable_previous_balance') ||
3069 $conf->exists('previous_balance-exclude_from_total')
3073 $total->{'total_item'} = &$embolden_function($item);
3074 $total->{'total_amount'} =
3075 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3076 if ( $multisection ) {
3077 if ( $adjust_section->{'sort_weight'} ) {
3078 $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3079 sprintf("%.2f", ($self->billing_balance || 0) );
3081 $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3082 sprintf('%.2f', $self->charged );
3085 push @total_items, $total;
3087 push @buf,['','-----------'];
3090 sprintf( '%10.2f', $amount )
3095 unless ( $conf->exists('disable_previous_balance') ) {
3096 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3099 my $credittotal = 0;
3100 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3103 $total->{'total_item'} = &$escape_function($credit->{'description'});
3104 $credittotal += $credit->{'amount'};
3105 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3106 $adjusttotal += $credit->{'amount'};
3107 if ( $multisection ) {
3108 my $money = $old_latex ? '' : $money_char;
3109 push @detail_items, {
3110 ext_description => [],
3113 description => &$escape_function($credit->{'description'}),
3114 amount => $money. $credit->{'amount'},
3116 section => $adjust_section,
3119 push @total_items, $total;
3123 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3126 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3127 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3131 my $paymenttotal = 0;
3132 foreach my $payment ( $self->_items_payments ) {
3134 $total->{'total_item'} = &$escape_function($payment->{'description'});
3135 $paymenttotal += $payment->{'amount'};
3136 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3137 $adjusttotal += $payment->{'amount'};
3138 if ( $multisection ) {
3139 my $money = $old_latex ? '' : $money_char;
3140 push @detail_items, {
3141 ext_description => [],
3144 description => &$escape_function($payment->{'description'}),
3145 amount => $money. $payment->{'amount'},
3147 section => $adjust_section,
3150 push @total_items, $total;
3152 push @buf, [ $payment->{'description'},
3153 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3156 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3158 if ( $multisection ) {
3159 $adjust_section->{'subtotal'} = $other_money_char.
3160 sprintf('%.2f', $adjusttotal);
3161 push @sections, $adjust_section
3162 unless $adjust_section->{sort_weight};
3167 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3168 $total->{'total_amount'} =
3169 &$embolden_function(
3170 $other_money_char. sprintf('%.2f', $summarypage
3172 $self->billing_balance
3173 : $self->owed + $pr_total
3176 if ( $multisection && !$adjust_section->{sort_weight} ) {
3177 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3178 $total->{'total_amount'};
3180 push @total_items, $total;
3182 push @buf,['','-----------'];
3183 push @buf,[$self->balance_due_msg, $money_char.
3184 sprintf("%10.2f", $balance_due ) ];
3187 if ( $conf->exists('previous_balance-show_credit')
3188 and $cust_main->balance < 0 ) {
3189 my $credit_total = {
3190 'total_item' => &$embolden_function($self->credit_balance_msg),
3191 'total_amount' => &$embolden_function(
3192 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3195 if ( $multisection ) {
3196 $adjust_section->{'posttotal'} .= $newline_token .
3197 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3200 push @total_items, $credit_total;
3202 push @buf,['','-----------'];
3203 push @buf,[$self->credit_balance_msg, $money_char.
3204 sprintf("%10.2f", -$cust_main->balance ) ];
3208 if ( $multisection ) {
3209 if ($conf->exists('svc_phone_sections')) {
3211 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3212 $total->{'total_amount'} =
3213 &$embolden_function(
3214 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3216 my $last_section = pop @sections;
3217 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3218 $total->{'total_amount'};
3219 push @sections, $last_section;
3221 push @sections, @$late_sections
3225 my @includelist = ();
3226 push @includelist, 'summary' if $summarypage;
3227 foreach my $include ( @includelist ) {
3229 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3232 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3234 @inc_src = $conf->config($inc_file, $agentnum);
3238 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3240 my $convert_map = $convert_maps{$format}{$include};
3242 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3243 s/--\@\]/$delimiters{$format}[1]/g;
3246 &$convert_map( $conf->config($inc_file, $agentnum) );
3250 my $inc_tt = new Text::Template (
3252 SOURCE => [ map "$_\n", @inc_src ],
3253 DELIMITERS => $delimiters{$format},
3254 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3256 unless ( $inc_tt->compile() ) {
3257 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3258 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3262 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3264 $invoice_data{$include} =~ s/\n+$//
3265 if ($format eq 'latex');
3270 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3271 /invoice_lines\((\d*)\)/;
3272 $invoice_lines += $1 || scalar(@buf);
3275 die "no invoice_lines() functions in template?"
3276 if ( $format eq 'template' && !$wasfunc );
3278 if ($format eq 'template') {
3280 if ( $invoice_lines ) {
3281 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3282 $invoice_data{'total_pages'}++
3283 if scalar(@buf) % $invoice_lines;
3286 #setup subroutine for the template
3287 sub FS::cust_bill::_template::invoice_lines {
3288 my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3290 scalar(@FS::cust_bill::_template::buf)
3291 ? shift @FS::cust_bill::_template::buf
3300 push @collect, split("\n",
3301 $text_template->fill_in( HASH => \%invoice_data,
3302 PACKAGE => 'FS::cust_bill::_template'
3305 $FS::cust_bill::_template::page++;
3307 map "$_\n", @collect;
3309 warn "filling in template for invoice ". $self->invnum. "\n"
3311 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3314 $text_template->fill_in(HASH => \%invoice_data);
3318 # helper routine for generating date ranges
3319 sub _prior_month30s {
3322 [ 1, 2592000 ], # 0-30 days ago
3323 [ 2592000, 5184000 ], # 30-60 days ago
3324 [ 5184000, 7776000 ], # 60-90 days ago
3325 [ 7776000, 0 ], # 90+ days ago
3328 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3329 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3334 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3336 Returns an postscript invoice, as a scalar.
3338 Options can be passed as a hashref (recommended) or as a list of time, template
3339 and then any key/value pairs for any other options.
3341 I<time> an optional value used to control the printing of overdue messages. The
3342 default is now. It isn't the date of the invoice; that's the `_date' field.
3343 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3344 L<Time::Local> and L<Date::Parse> for conversion functions.
3346 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3353 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3354 my $ps = generate_ps($file);
3356 unlink($barcodefile) if $barcodefile;
3361 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3363 Returns an PDF invoice, as a scalar.
3365 Options can be passed as a hashref (recommended) or as a list of time, template
3366 and then any key/value pairs for any other options.
3368 I<time> an optional value used to control the printing of overdue messages. The
3369 default is now. It isn't the date of the invoice; that's the `_date' field.
3370 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3371 L<Time::Local> and L<Date::Parse> for conversion functions.
3373 I<template>, if specified, is the name of a suffix for alternate invoices.
3375 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3382 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3383 my $pdf = generate_pdf($file);
3385 unlink($barcodefile) if $barcodefile;
3390 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3392 Returns an HTML invoice, as a scalar.
3394 I<time> an optional value used to control the printing of overdue messages. The
3395 default is now. It isn't the date of the invoice; that's the `_date' field.
3396 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3397 L<Time::Local> and L<Date::Parse> for conversion functions.
3399 I<template>, if specified, is the name of a suffix for alternate invoices.
3401 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3403 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3404 when emailing the invoice as part of a multipart/related MIME email.
3412 %params = %{ shift() };
3414 $params{'time'} = shift;
3415 $params{'template'} = shift;
3416 $params{'cid'} = shift;
3419 $params{'format'} = 'html';
3421 $self->print_generic( %params );
3424 # quick subroutine for print_latex
3426 # There are ten characters that LaTeX treats as special characters, which
3427 # means that they do not simply typeset themselves:
3428 # # $ % & ~ _ ^ \ { }
3430 # TeX ignores blanks following an escaped character; if you want a blank (as
3431 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3435 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3436 $value =~ s/([<>])/\$$1\$/g;
3442 encode_entities($value);
3446 sub _html_escape_nbsp {
3447 my $value = _html_escape(shift);
3448 $value =~ s/ +/ /g;
3452 #utility methods for print_*
3454 sub _translate_old_latex_format {
3455 warn "_translate_old_latex_format called\n"
3462 if ( $line =~ /^%%Detail\s*$/ ) {
3464 push @template, q![@--!,
3465 q! foreach my $_tr_line (@detail_items) {!,
3466 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3467 q! $_tr_line->{'description'} .= !,
3468 q! "\\tabularnewline\n~~".!,
3469 q! join( "\\tabularnewline\n~~",!,
3470 q! @{$_tr_line->{'ext_description'}}!,
3474 while ( ( my $line_item_line = shift )
3475 !~ /^%%EndDetail\s*$/ ) {
3476 $line_item_line =~ s/'/\\'/g; # nice LTS
3477 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3478 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3479 push @template, " \$OUT .= '$line_item_line';";
3482 push @template, '}',
3485 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3487 push @template, '[@--',
3488 ' foreach my $_tr_line (@total_items) {';
3490 while ( ( my $total_item_line = shift )
3491 !~ /^%%EndTotalDetails\s*$/ ) {
3492 $total_item_line =~ s/'/\\'/g; # nice LTS
3493 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3494 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3495 push @template, " \$OUT .= '$total_item_line';";
3498 push @template, '}',
3502 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3503 push @template, $line;
3509 warn "$_\n" foreach @template;
3518 #check for an invoice-specific override
3519 return $self->invoice_terms if $self->invoice_terms;
3521 #check for a customer- specific override
3522 my $cust_main = $self->cust_main;
3523 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3525 #use configured default
3526 $conf->config('invoice_default_terms') || '';
3532 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3533 $duedate = $self->_date() + ( $1 * 86400 );
3540 $self->due_date ? time2str(shift, $self->due_date) : '';
3543 sub balance_due_msg {
3545 my $msg = 'Balance Due';
3546 return $msg unless $self->terms;
3547 if ( $self->due_date ) {
3548 $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3549 } elsif ( $self->terms ) {
3550 $msg .= ' - '. $self->terms;
3555 sub balance_due_date {
3558 if ( $conf->exists('invoice_default_terms')
3559 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3560 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3565 sub credit_balance_msg { 'Credit Balance Remaining' }
3567 =item invnum_date_pretty
3569 Returns a string with the invoice number and date, for example:
3570 "Invoice #54 (3/20/2008)"
3574 sub invnum_date_pretty {
3576 'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3581 Returns a string with the date, for example: "3/20/2008"
3587 time2str($date_format, $self->_date);
3590 use vars qw(%pkg_category_cache);
3591 sub _items_sections {
3594 my $summarypage = shift;
3596 my $extra_sections = shift;
3600 my %late_subtotal = ();
3603 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3606 my $usage = $cust_bill_pkg->usage;
3608 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3609 next if ( $display->summary && $summarypage );
3611 my $section = $display->section;
3612 my $type = $display->type;
3614 $not_tax{$section} = 1
3615 unless $cust_bill_pkg->pkgnum == 0;
3617 if ( $display->post_total && !$summarypage ) {
3618 if (! $type || $type eq 'S') {
3619 $late_subtotal{$section} += $cust_bill_pkg->setup
3620 if $cust_bill_pkg->setup != 0;
3624 $late_subtotal{$section} += $cust_bill_pkg->recur
3625 if $cust_bill_pkg->recur != 0;
3628 if ($type && $type eq 'R') {
3629 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3630 if $cust_bill_pkg->recur != 0;
3633 if ($type && $type eq 'U') {
3634 $late_subtotal{$section} += $usage
3635 unless scalar(@$extra_sections);
3640 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3642 if (! $type || $type eq 'S') {
3643 $subtotal{$section} += $cust_bill_pkg->setup
3644 if $cust_bill_pkg->setup != 0;
3648 $subtotal{$section} += $cust_bill_pkg->recur
3649 if $cust_bill_pkg->recur != 0;
3652 if ($type && $type eq 'R') {
3653 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3654 if $cust_bill_pkg->recur != 0;
3657 if ($type && $type eq 'U') {
3658 $subtotal{$section} += $usage
3659 unless scalar(@$extra_sections);
3668 %pkg_category_cache = ();
3670 push @$late, map { { 'description' => &{$escape}($_),
3671 'subtotal' => $late_subtotal{$_},
3673 'sort_weight' => ( _pkg_category($_)
3674 ? _pkg_category($_)->weight
3677 ((_pkg_category($_) && _pkg_category($_)->condense)
3678 ? $self->_condense_section($format)
3682 sort _sectionsort keys %late_subtotal;
3685 if ( $summarypage ) {
3686 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3687 map { $_->categoryname } qsearch('pkg_category', {});
3688 push @sections, '' if exists($subtotal{''});
3690 @sections = keys %subtotal;
3693 my @early = map { { 'description' => &{$escape}($_),
3694 'subtotal' => $subtotal{$_},
3695 'summarized' => $not_tax{$_} ? '' : 'Y',
3696 'tax_section' => $not_tax{$_} ? '' : 'Y',
3697 'sort_weight' => ( _pkg_category($_)
3698 ? _pkg_category($_)->weight
3701 ((_pkg_category($_) && _pkg_category($_)->condense)
3702 ? $self->_condense_section($format)
3707 push @early, @$extra_sections if $extra_sections;
3709 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3713 #helper subs for above
3716 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3720 my $categoryname = shift;
3721 $pkg_category_cache{$categoryname} ||=
3722 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3725 my %condensed_format = (
3726 'label' => [ qw( Description Qty Amount ) ],
3728 sub { shift->{description} },
3729 sub { shift->{quantity} },
3730 sub { my($href, %opt) = @_;
3731 ($opt{dollar} || ''). $href->{amount};
3734 'align' => [ qw( l r r ) ],
3735 'span' => [ qw( 5 1 1 ) ], # unitprices?
3736 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3739 sub _condense_section {
3740 my ( $self, $format ) = ( shift, shift );
3742 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3743 qw( description_generator
3746 total_line_generator
3751 sub _condensed_generator_defaults {
3752 my ( $self, $format ) = ( shift, shift );
3753 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3762 sub _condensed_header_generator {
3763 my ( $self, $format ) = ( shift, shift );
3765 my ( $f, $prefix, $suffix, $separator, $column ) =
3766 _condensed_generator_defaults($format);
3768 if ($format eq 'latex') {
3769 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3770 $suffix = "\\\\\n\\hline";
3773 sub { my ($d,$a,$s,$w) = @_;
3774 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3776 } elsif ( $format eq 'html' ) {
3777 $prefix = '<th></th>';
3781 sub { my ($d,$a,$s,$w) = @_;
3782 return qq!<th align="$html_align{$a}">$d</th>!;
3790 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3792 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3795 $prefix. join($separator, @result). $suffix;
3800 sub _condensed_description_generator {
3801 my ( $self, $format ) = ( shift, shift );
3803 my ( $f, $prefix, $suffix, $separator, $column ) =
3804 _condensed_generator_defaults($format);
3806 my $money_char = '$';
3807 if ($format eq 'latex') {
3808 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3810 $separator = " & \n";
3812 sub { my ($d,$a,$s,$w) = @_;
3813 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3815 $money_char = '\\dollar';
3816 }elsif ( $format eq 'html' ) {
3817 $prefix = '"><td align="center"></td>';
3821 sub { my ($d,$a,$s,$w) = @_;
3822 return qq!<td align="$html_align{$a}">$d</td>!;
3824 #$money_char = $conf->config('money_char') || '$';
3825 $money_char = ''; # this is madness
3833 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3835 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3837 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3838 map { $f->{$_}->[$i] } qw(align span width)
3842 $prefix. join( $separator, @result ). $suffix;
3847 sub _condensed_total_generator {
3848 my ( $self, $format ) = ( shift, shift );
3850 my ( $f, $prefix, $suffix, $separator, $column ) =
3851 _condensed_generator_defaults($format);
3854 if ($format eq 'latex') {
3857 $separator = " & \n";
3859 sub { my ($d,$a,$s,$w) = @_;
3860 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3862 }elsif ( $format eq 'html' ) {
3866 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3868 sub { my ($d,$a,$s,$w) = @_;
3869 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3878 # my $r = &{$f->{fields}->[$i]}(@args);
3879 # $r .= ' Total' unless $i;
3881 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3883 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3884 map { $f->{$_}->[$i] } qw(align span width)
3888 $prefix. join( $separator, @result ). $suffix;
3893 =item total_line_generator FORMAT
3895 Returns a coderef used for generation of invoice total line items for this
3896 usage_class. FORMAT is either html or latex
3900 # should not be used: will have issues with hash element names (description vs
3901 # total_item and amount vs total_amount -- another array of functions?
3903 sub _condensed_total_line_generator {
3904 my ( $self, $format ) = ( shift, shift );
3906 my ( $f, $prefix, $suffix, $separator, $column ) =
3907 _condensed_generator_defaults($format);
3910 if ($format eq 'latex') {
3913 $separator = " & \n";
3915 sub { my ($d,$a,$s,$w) = @_;
3916 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3918 }elsif ( $format eq 'html' ) {
3922 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3924 sub { my ($d,$a,$s,$w) = @_;
3925 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3934 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3936 &{$column}( &{$f->{fields}->[$i]}(@args),
3937 map { $f->{$_}->[$i] } qw(align span width)
3941 $prefix. join( $separator, @result ). $suffix;
3946 #sub _items_extra_usage_sections {
3948 # my $escape = shift;
3950 # my %sections = ();
3952 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
3953 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3955 # next unless $cust_bill_pkg->pkgnum > 0;
3957 # foreach my $section ( keys %usage_class ) {
3959 # my $usage = $cust_bill_pkg->usage($section);
3961 # next unless $usage && $usage > 0;
3963 # $sections{$section} ||= 0;
3964 # $sections{$section} += $usage;
3970 # map { { 'description' => &{$escape}($_),
3971 # 'subtotal' => $sections{$_},
3972 # 'summarized' => '',
3973 # 'tax_section' => '',
3976 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3980 sub _items_extra_usage_sections {
3989 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3990 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3991 next unless $cust_bill_pkg->pkgnum > 0;
3993 foreach my $classnum ( keys %usage_class ) {
3994 my $section = $usage_class{$classnum}->classname;
3995 $classnums{$section} = $classnum;
3997 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3998 my $amount = $detail->amount;
3999 next unless $amount && $amount > 0;
4001 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4002 $sections{$section}{amount} += $amount; #subtotal
4003 $sections{$section}{calls}++;
4004 $sections{$section}{duration} += $detail->duration;
4006 my $desc = $detail->regionname;
4007 my $description = $desc;
4008 $description = substr($desc, 0, 50). '...'
4009 if $format eq 'latex' && length($desc) > 50;
4011 $lines{$section}{$desc} ||= {
4012 description => &{$escape}($description),
4013 #pkgpart => $part_pkg->pkgpart,
4014 pkgnum => $cust_bill_pkg->pkgnum,
4019 #unit_amount => $cust_bill_pkg->unitrecur,
4020 quantity => $cust_bill_pkg->quantity,
4021 product_code => 'N/A',
4022 ext_description => [],
4025 $lines{$section}{$desc}{amount} += $amount;
4026 $lines{$section}{$desc}{calls}++;
4027 $lines{$section}{$desc}{duration} += $detail->duration;
4033 my %sectionmap = ();
4034 foreach (keys %sections) {
4035 my $usage_class = $usage_class{$classnums{$_}};
4036 $sectionmap{$_} = { 'description' => &{$escape}($_),
4037 'amount' => $sections{$_}{amount}, #subtotal
4038 'calls' => $sections{$_}{calls},
4039 'duration' => $sections{$_}{duration},
4041 'tax_section' => '',
4042 'sort_weight' => $usage_class->weight,
4043 ( $usage_class->format
4044 ? ( map { $_ => $usage_class->$_($format) }
4045 qw( description_generator header_generator total_generator total_line_generator )
4052 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4056 foreach my $section ( keys %lines ) {
4057 foreach my $line ( keys %{$lines{$section}} ) {
4058 my $l = $lines{$section}{$line};
4059 $l->{section} = $sectionmap{$section};
4060 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4061 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4066 return(\@sections, \@lines);
4072 my $end = $self->_date;
4074 # start at date of previous invoice + 1 second or 0 if no previous invoice
4075 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4076 $start = 0 if !$start;
4079 my $cust_main = $self->cust_main;
4080 my @pkgs = $cust_main->all_pkgs;
4081 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4084 foreach my $pkg ( @pkgs ) {
4085 my @h_cust_svc = $pkg->h_cust_svc($end);
4086 foreach my $h_cust_svc ( @h_cust_svc ) {
4087 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4088 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4090 my $inserted = $h_cust_svc->date_inserted;
4091 my $deleted = $h_cust_svc->date_deleted;
4092 my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4094 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4096 # DID either activated or ported in; cannot be both for same DID simultaneously
4097 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4098 && (!$phone_inserted->lnp_status
4099 || $phone_inserted->lnp_status eq ''
4100 || $phone_inserted->lnp_status eq 'native')) {
4103 else { # this one not so clean, should probably move to (h_)svc_phone
4104 my $phone_portedin = qsearchs( 'h_svc_phone',
4105 { 'svcnum' => $h_cust_svc->svcnum,
4106 'lnp_status' => 'portedin' },
4107 FS::h_svc_phone->sql_h_searchs($end),
4109 $num_portedin++ if $phone_portedin;
4112 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4113 if($deleted >= $start && $deleted <= $end && $phone_deleted
4114 && (!$phone_deleted->lnp_status
4115 || $phone_deleted->lnp_status ne 'portingout')) {
4118 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4119 && $phone_deleted->lnp_status
4120 && $phone_deleted->lnp_status eq 'portingout') {
4124 # increment usage minutes
4125 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4126 foreach my $cdr ( @cdrs ) {
4127 $minutes += $cdr->billsec/60;
4130 # don't look at this service again
4131 push @seen, $h_cust_svc->svcnum;
4135 $minutes = sprintf("%d", $minutes);
4136 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4137 . "$num_deactivated Ported-Out: $num_portedout ",
4138 "Total Minutes: $minutes");
4141 sub _items_accountcode_cdr {
4146 my $section = { 'amount' => 0,
4149 'sort_weight' => '',
4151 'description' => 'Usage by Account Code',
4154 'total_generator' => sub { '' },
4158 my %accountcodes = ();
4160 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4161 next unless $cust_bill_pkg->pkgnum > 0;
4163 my @header = $cust_bill_pkg->details_header;
4164 next unless scalar(@header);
4165 $section->{'header'} = join(',',@header);
4167 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4169 $section->{'header'} = $detail->formatted('format' => $format)
4170 if($detail->detail eq $section->{'header'});
4172 my $accountcode = $detail->accountcode;
4173 next unless $accountcode;
4175 my $amount = $detail->amount;
4176 next unless $amount && $amount > 0;
4178 $accountcodes{$accountcode} ||= {
4179 description => $accountcode,
4186 product_code => 'N/A',
4187 section => $section,
4188 ext_description => [],
4191 $accountcodes{$accountcode}{'amount'} += $amount;
4192 $accountcodes{$accountcode}{calls}++;
4193 $accountcodes{$accountcode}{duration} += $detail->duration;
4194 push @{$accountcodes{$accountcode}{ext_description}},
4195 $detail->formatted('format' => $format);
4199 foreach my $l ( values %accountcodes ) {
4200 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4201 unshift @{$l->{ext_description}}, $section->{'header'};
4205 return ($section,\@lines);
4208 sub _items_svc_phone_sections {
4217 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4218 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4220 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4221 next unless $cust_bill_pkg->pkgnum > 0;
4223 my @header = $cust_bill_pkg->details_header;
4224 next unless scalar(@header);
4226 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4228 my $phonenum = $detail->phonenum;
4229 next unless $phonenum;
4231 my $amount = $detail->amount;
4232 next unless $amount && $amount > 0;
4234 $sections{$phonenum} ||= { 'amount' => 0,
4237 'sort_weight' => -1,
4238 'phonenum' => $phonenum,
4240 $sections{$phonenum}{amount} += $amount; #subtotal
4241 $sections{$phonenum}{calls}++;
4242 $sections{$phonenum}{duration} += $detail->duration;
4244 my $desc = $detail->regionname;
4245 my $description = $desc;
4246 $description = substr($desc, 0, 50). '...'
4247 if $format eq 'latex' && length($desc) > 50;
4249 $lines{$phonenum}{$desc} ||= {
4250 description => &{$escape}($description),
4251 #pkgpart => $part_pkg->pkgpart,
4259 product_code => 'N/A',
4260 ext_description => [],
4263 $lines{$phonenum}{$desc}{amount} += $amount;
4264 $lines{$phonenum}{$desc}{calls}++;
4265 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4267 my $line = $usage_class{$detail->classnum}->classname;
4268 $sections{"$phonenum $line"} ||=
4272 'sort_weight' => $usage_class{$detail->classnum}->weight,
4273 'phonenum' => $phonenum,
4274 'header' => [ @header ],
4276 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4277 $sections{"$phonenum $line"}{calls}++;
4278 $sections{"$phonenum $line"}{duration} += $detail->duration;
4280 $lines{"$phonenum $line"}{$desc} ||= {
4281 description => &{$escape}($description),
4282 #pkgpart => $part_pkg->pkgpart,
4290 product_code => 'N/A',
4291 ext_description => [],
4294 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4295 $lines{"$phonenum $line"}{$desc}{calls}++;
4296 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4297 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4298 $detail->formatted('format' => $format);
4303 my %sectionmap = ();
4304 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4305 foreach ( keys %sections ) {
4306 my @header = @{ $sections{$_}{header} || [] };
4308 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4309 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4310 my $usage_class = $summary ? $simple : $usage_simple;
4311 my $ending = $summary ? ' usage charges' : '';
4314 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4316 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4317 'amount' => $sections{$_}{amount}, #subtotal
4318 'calls' => $sections{$_}{calls},
4319 'duration' => $sections{$_}{duration},
4321 'tax_section' => '',
4322 'phonenum' => $sections{$_}{phonenum},
4323 'sort_weight' => $sections{$_}{sort_weight},
4324 'post_total' => $summary, #inspire pagebreak
4326 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4327 qw( description_generator
4330 total_line_generator
4337 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4338 $a->{sort_weight} <=> $b->{sort_weight}
4343 foreach my $section ( keys %lines ) {
4344 foreach my $line ( keys %{$lines{$section}} ) {
4345 my $l = $lines{$section}{$line};
4346 $l->{section} = $sectionmap{$section};
4347 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4348 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4353 if($conf->exists('phone_usage_class_summary')) {
4354 # this only works with Latex
4358 # after this, we'll have only two sections per DID:
4359 # Calls Summary and Calls Detail
4360 foreach my $section ( @sections ) {
4361 if($section->{'post_total'}) {
4362 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4363 $section->{'total_line_generator'} = sub { '' };
4364 $section->{'total_generator'} = sub { '' };
4365 $section->{'header_generator'} = sub { '' };
4366 $section->{'description_generator'} = '';
4367 push @newsections, $section;
4368 my %calls_detail = %$section;
4369 $calls_detail{'post_total'} = '';
4370 $calls_detail{'sort_weight'} = '';
4371 $calls_detail{'description_generator'} = sub { '' };
4372 $calls_detail{'header_generator'} = sub {
4373 return ' & Date/Time & Called Number & Duration & Price'
4374 if $format eq 'latex';
4377 $calls_detail{'description'} = 'Calls Detail: '
4378 . $section->{'phonenum'};
4379 push @newsections, \%calls_detail;
4383 # after this, each usage class is collapsed/summarized into a single
4384 # line under the Calls Summary section
4385 foreach my $newsection ( @newsections ) {
4386 if($newsection->{'post_total'}) { # this means Calls Summary
4387 foreach my $section ( @sections ) {
4388 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4389 && !$section->{'post_total'});
4390 my $newdesc = $section->{'description'};
4391 my $tn = $section->{'phonenum'};
4392 $newdesc =~ s/$tn//g;
4393 my $line = { ext_description => [],
4397 calls => $section->{'calls'},
4398 section => $newsection,
4399 duration => $section->{'duration'},
4400 description => $newdesc,
4401 amount => sprintf("%.2f",$section->{'amount'}),
4402 product_code => 'N/A',
4404 push @newlines, $line;
4409 # after this, Calls Details is populated with all CDRs
4410 foreach my $newsection ( @newsections ) {
4411 if(!$newsection->{'post_total'}) { # this means Calls Details
4412 foreach my $line ( @lines ) {
4413 next unless (scalar(@{$line->{'ext_description'}}) &&
4414 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4416 my @extdesc = @{$line->{'ext_description'}};
4418 foreach my $extdesc ( @extdesc ) {
4419 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4420 push @newextdesc, $extdesc;
4422 $line->{'ext_description'} = \@newextdesc;
4423 $line->{'section'} = $newsection;
4424 push @newlines, $line;
4429 return(\@newsections, \@newlines);
4432 return(\@sections, \@lines);
4439 #my @display = scalar(@_)
4441 # : qw( _items_previous _items_pkg );
4442 # #: qw( _items_pkg );
4443 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4444 my @display = qw( _items_previous _items_pkg );
4447 foreach my $display ( @display ) {
4448 push @b, $self->$display(@_);
4453 sub _items_previous {
4455 my $cust_main = $self->cust_main;
4456 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4458 foreach ( @pr_cust_bill ) {
4459 my $date = $conf->exists('invoice_show_prior_due_date')
4460 ? 'due '. $_->due_date2str($date_format)
4461 : time2str($date_format, $_->_date);
4463 'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4464 #'pkgpart' => 'N/A',
4466 'amount' => sprintf("%.2f", $_->owed),
4472 # 'description' => 'Previous Balance',
4473 # #'pkgpart' => 'N/A',
4474 # 'pkgnum' => 'N/A',
4475 # 'amount' => sprintf("%10.2f", $pr_total ),
4476 # 'ext_description' => [ map {
4477 # "Invoice ". $_->invnum.
4478 # " (". time2str("%x",$_->_date). ") ".
4479 # sprintf("%10.2f", $_->owed)
4480 # } @pr_cust_bill ],
4489 warn "$me _items_pkg searching for all package line items\n"
4492 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4494 warn "$me _items_pkg filtering line items\n"
4496 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4498 if ($options{section} && $options{section}->{condensed}) {
4500 warn "$me _items_pkg condensing section\n"
4504 local $Storable::canonical = 1;
4505 foreach ( @items ) {
4507 delete $item->{ref};
4508 delete $item->{ext_description};
4509 my $key = freeze($item);
4510 $itemshash{$key} ||= 0;
4511 $itemshash{$key} ++; # += $item->{quantity};
4513 @items = sort { $a->{description} cmp $b->{description} }
4514 map { my $i = thaw($_);
4515 $i->{quantity} = $itemshash{$_};
4517 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4523 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4530 return 0 unless $a->itemdesc cmp $b->itemdesc;
4531 return -1 if $b->itemdesc eq 'Tax';
4532 return 1 if $a->itemdesc eq 'Tax';
4533 return -1 if $b->itemdesc eq 'Other surcharges';
4534 return 1 if $a->itemdesc eq 'Other surcharges';
4535 $a->itemdesc cmp $b->itemdesc;
4540 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4541 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4544 sub _items_cust_bill_pkg {
4546 my $cust_bill_pkgs = shift;
4549 my $format = $opt{format} || '';
4550 my $escape_function = $opt{escape_function} || sub { shift };
4551 my $format_function = $opt{format_function} || '';
4552 my $unsquelched = $opt{unsquelched} || '';
4553 my $section = $opt{section}->{description} if $opt{section};
4554 my $summary_page = $opt{summary_page} || '';
4555 my $multilocation = $opt{multilocation} || '';
4556 my $multisection = $opt{multisection} || '';
4557 my $discount_show_always = 0;
4560 my ($s, $r, $u) = ( undef, undef, undef );
4561 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4564 warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4567 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4568 && $conf->exists('discount-show-always'));
4570 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4571 if ( $_ && !$cust_bill_pkg->hidden ) {
4572 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4573 $_->{amount} =~ s/^\-0\.00$/0.00/;
4574 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4576 unless ( $_->{amount} == 0 && !$discount_show_always );
4581 foreach my $display ( grep { defined($section)
4582 ? $_->section eq $section
4585 #grep { !$_->summary || !$summary_page } # bunk!
4586 grep { !$_->summary || $multisection }
4587 $cust_bill_pkg->cust_bill_pkg_display
4591 warn "$me _items_cust_bill_pkg considering display item $display\n"
4594 my $type = $display->type;
4596 my $desc = $cust_bill_pkg->desc;
4597 $desc = substr($desc, 0, 50). '...'
4598 if $format eq 'latex' && length($desc) > 50;
4600 my %details_opt = ( 'format' => $format,
4601 'escape_function' => $escape_function,
4602 'format_function' => $format_function,
4605 if ( $cust_bill_pkg->pkgnum > 0 ) {
4607 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4610 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4612 if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4614 warn "$me _items_cust_bill_pkg adding setup\n"
4617 my $description = $desc;
4618 $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4621 unless ( $cust_pkg->part_pkg->hide_svc_detail
4622 || $cust_bill_pkg->hidden )
4625 push @d, map &{$escape_function}($_),
4626 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4627 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4629 if ( $multilocation ) {
4630 my $loc = $cust_pkg->location_label;
4631 $loc = substr($loc, 0, 50). '...'
4632 if $format eq 'latex' && length($loc) > 50;
4633 push @d, &{$escape_function}($loc);
4638 push @d, $cust_bill_pkg->details(%details_opt)
4639 if $cust_bill_pkg->recur == 0;
4641 if ( $cust_bill_pkg->hidden ) {
4642 $s->{amount} += $cust_bill_pkg->setup;
4643 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4644 push @{ $s->{ext_description} }, @d;
4647 description => $description,
4648 #pkgpart => $part_pkg->pkgpart,
4649 pkgnum => $cust_bill_pkg->pkgnum,
4650 amount => $cust_bill_pkg->setup,
4651 unit_amount => $cust_bill_pkg->unitsetup,
4652 quantity => $cust_bill_pkg->quantity,
4653 ext_description => \@d,
4659 if ( ( $cust_bill_pkg->recur != 0 || $cust_bill_pkg->setup == 0 ||
4660 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4661 ( !$type || $type eq 'R' || $type eq 'U' )
4665 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4668 my $is_summary = $display->summary;
4669 my $description = ($is_summary && $type && $type eq 'U')
4670 ? "Usage charges" : $desc;
4672 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4673 " - ". time2str($date_format, $cust_bill_pkg->edate).
4675 unless $conf->exists('disable_line_item_date_ranges');
4679 #at least until cust_bill_pkg has "past" ranges in addition to
4680 #the "future" sdate/edate ones... see #3032
4681 my @dates = ( $self->_date );
4682 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4683 push @dates, $prev->sdate if $prev;
4684 push @dates, undef if !$prev;
4686 unless ( $cust_pkg->part_pkg->hide_svc_detail
4687 || $cust_bill_pkg->itemdesc
4688 || $cust_bill_pkg->hidden
4689 || $is_summary && $type && $type eq 'U' )
4692 warn "$me _items_cust_bill_pkg adding service details\n"
4695 push @d, map &{$escape_function}($_),
4696 $cust_pkg->h_labels_short(@dates, 'I')
4697 #$cust_bill_pkg->edate,
4698 #$cust_bill_pkg->sdate)
4699 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4701 warn "$me _items_cust_bill_pkg done adding service details\n"
4704 if ( $multilocation ) {
4705 my $loc = $cust_pkg->location_label;
4706 $loc = substr($loc, 0, 50). '...'
4707 if $format eq 'latex' && length($loc) > 50;
4708 push @d, &{$escape_function}($loc);
4713 unless ( $is_summary ) {
4714 warn "$me _items_cust_bill_pkg adding details\n"
4717 #instead of omitting details entirely in this case (unwanted side
4718 # effects), just omit CDRs
4719 $details_opt{'format_function'} = sub { () }
4720 if $type && $type eq 'R';
4722 push @d, $cust_bill_pkg->details(%details_opt);
4725 warn "$me _items_cust_bill_pkg calculating amount\n"
4730 $amount = $cust_bill_pkg->recur;
4731 } elsif ($type eq 'R') {
4732 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4733 } elsif ($type eq 'U') {
4734 $amount = $cust_bill_pkg->usage;
4737 if ( !$type || $type eq 'R' ) {
4739 warn "$me _items_cust_bill_pkg adding recur\n"
4742 if ( $cust_bill_pkg->hidden ) {
4743 $r->{amount} += $amount;
4744 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4745 push @{ $r->{ext_description} }, @d;
4748 description => $description,
4749 #pkgpart => $part_pkg->pkgpart,
4750 pkgnum => $cust_bill_pkg->pkgnum,
4752 unit_amount => $cust_bill_pkg->unitrecur,
4753 quantity => $cust_bill_pkg->quantity,
4754 ext_description => \@d,
4758 } else { # $type eq 'U'
4760 warn "$me _items_cust_bill_pkg adding usage\n"
4763 if ( $cust_bill_pkg->hidden ) {
4764 $u->{amount} += $amount;
4765 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4766 push @{ $u->{ext_description} }, @d;
4769 description => $description,
4770 #pkgpart => $part_pkg->pkgpart,
4771 pkgnum => $cust_bill_pkg->pkgnum,
4773 unit_amount => $cust_bill_pkg->unitrecur,
4774 quantity => $cust_bill_pkg->quantity,
4775 ext_description => \@d,
4780 } # recurring or usage with recurring charge
4782 } else { #pkgnum tax or one-shot line item (??)
4784 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4787 if ( $cust_bill_pkg->setup != 0 ) {
4789 'description' => $desc,
4790 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
4793 if ( $cust_bill_pkg->recur != 0 ) {
4795 'description' => "$desc (".
4796 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4797 time2str($date_format, $cust_bill_pkg->edate). ')',
4798 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
4808 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4811 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4813 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4814 $_->{amount} =~ s/^\-0\.00$/0.00/;
4815 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4817 unless ( $_->{amount} == 0 && !$discount_show_always );
4825 sub _items_credits {
4826 my( $self, %opt ) = @_;
4827 my $trim_len = $opt{'trim_len'} || 60;
4831 foreach ( $self->cust_credited ) {
4833 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4835 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4836 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4837 $reason = " ($reason) " if $reason;
4840 #'description' => 'Credit ref\#'. $_->crednum.
4841 # " (". time2str("%x",$_->cust_credit->_date) .")".
4843 'description' => 'Credit applied '.
4844 time2str($date_format,$_->cust_credit->_date). $reason,
4845 'amount' => sprintf("%.2f",$_->amount),
4853 sub _items_payments {
4857 #get & print payments
4858 foreach ( $self->cust_bill_pay ) {
4860 #something more elaborate if $_->amount ne ->cust_pay->paid ?
4863 'description' => "Payment received ".
4864 time2str($date_format,$_->cust_pay->_date ),
4865 'amount' => sprintf("%.2f", $_->amount )
4873 =item call_details [ OPTION => VALUE ... ]
4875 Returns an array of CSV strings representing the call details for this invoice
4876 The only option available is the boolean prepend_billed_number
4881 my ($self, %opt) = @_;
4883 my $format_function = sub { shift };
4885 if ($opt{prepend_billed_number}) {
4886 $format_function = sub {
4890 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4895 my @details = map { $_->details( 'format_function' => $format_function,
4896 'escape_function' => sub{ return() },
4900 $self->cust_bill_pkg;
4901 my $header = $details[0];
4902 ( $header, grep { $_ ne $header } @details );
4912 =item process_reprint
4916 sub process_reprint {
4917 process_re_X('print', @_);
4920 =item process_reemail
4924 sub process_reemail {
4925 process_re_X('email', @_);
4933 process_re_X('fax', @_);
4941 process_re_X('ftp', @_);
4948 sub process_respool {
4949 process_re_X('spool', @_);
4952 use Storable qw(thaw);
4956 my( $method, $job ) = ( shift, shift );
4957 warn "$me process_re_X $method for job $job\n" if $DEBUG;
4959 my $param = thaw(decode_base64(shift));
4960 warn Dumper($param) if $DEBUG;
4971 my($method, $job, %param ) = @_;
4973 warn "re_X $method for job $job with param:\n".
4974 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
4977 #some false laziness w/search/cust_bill.html
4979 my $orderby = 'ORDER BY cust_bill._date';
4981 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4983 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4985 my @cust_bill = qsearch( {
4986 #'select' => "cust_bill.*",
4987 'table' => 'cust_bill',
4988 'addl_from' => $addl_from,
4990 'extra_sql' => $extra_sql,
4991 'order_by' => $orderby,
4995 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4997 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5000 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5001 foreach my $cust_bill ( @cust_bill ) {
5002 $cust_bill->$method();
5004 if ( $job ) { #progressbar foo
5006 if ( time - $min_sec > $last ) {
5007 my $error = $job->update_statustext(
5008 int( 100 * $num / scalar(@cust_bill) )
5010 die $error if $error;
5021 =head1 CLASS METHODS
5027 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5032 my ($class, $start, $end) = @_;
5034 $class->paid_sql($start, $end). ' - '.
5035 $class->credited_sql($start, $end);
5040 Returns an SQL fragment to retreive the net amount (charged minus credited).
5045 my ($class, $start, $end) = @_;
5046 'charged - '. $class->credited_sql($start, $end);
5051 Returns an SQL fragment to retreive the amount paid against this invoice.
5056 my ($class, $start, $end) = @_;
5057 $start &&= "AND cust_bill_pay._date <= $start";
5058 $end &&= "AND cust_bill_pay._date > $end";
5059 $start = '' unless defined($start);
5060 $end = '' unless defined($end);
5061 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5062 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5067 Returns an SQL fragment to retreive the amount credited against this invoice.
5072 my ($class, $start, $end) = @_;
5073 $start &&= "AND cust_credit_bill._date <= $start";
5074 $end &&= "AND cust_credit_bill._date > $end";
5075 $start = '' unless defined($start);
5076 $end = '' unless defined($end);
5077 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5078 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5083 Returns an SQL fragment to retrieve the due date of an invoice.
5084 Currently only supported on PostgreSQL.
5092 cust_bill.invoice_terms,
5093 cust_main.invoice_terms,
5094 \''.($conf->config('invoice_default_terms') || '').'\'
5095 ), E\'Net (\\\\d+)\'
5097 ) * 86400 + cust_bill._date'
5100 =item search_sql_where HASHREF
5102 Class method which returns an SQL WHERE fragment to search for parameters
5103 specified in HASHREF. Valid parameters are
5109 List reference of start date, end date, as UNIX timestamps.
5119 List reference of charged limits (exclusive).
5123 List reference of charged limits (exclusive).
5127 flag, return open invoices only
5131 flag, return net invoices only
5135 =item newest_percust
5139 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5143 sub search_sql_where {
5144 my($class, $param) = @_;
5146 warn "$me search_sql_where called with params: \n".
5147 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5153 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5154 push @search, "cust_main.agentnum = $1";
5158 if ( $param->{_date} ) {
5159 my($beginning, $ending) = @{$param->{_date}};
5161 push @search, "cust_bill._date >= $beginning",
5162 "cust_bill._date < $ending";
5166 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5167 push @search, "cust_bill.invnum >= $1";
5169 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5170 push @search, "cust_bill.invnum <= $1";
5174 if ( $param->{charged} ) {
5175 my @charged = ref($param->{charged})
5176 ? @{ $param->{charged} }
5177 : ($param->{charged});
5179 push @search, map { s/^charged/cust_bill.charged/; $_; }
5183 my $owed_sql = FS::cust_bill->owed_sql;
5186 if ( $param->{owed} ) {
5187 my @owed = ref($param->{owed})
5188 ? @{ $param->{owed} }
5190 push @search, map { s/^owed/$owed_sql/; $_; }
5195 push @search, "0 != $owed_sql"
5196 if $param->{'open'};
5197 push @search, '0 != '. FS::cust_bill->net_sql
5201 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5202 if $param->{'days'};
5205 if ( $param->{'newest_percust'} ) {
5207 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5208 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5210 my @newest_where = map { my $x = $_;
5211 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5214 grep ! /^cust_main./, @search;
5215 my $newest_where = scalar(@newest_where)
5216 ? ' AND '. join(' AND ', @newest_where)
5220 push @search, "cust_bill._date = (
5221 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5222 WHERE newest_cust_bill.custnum = cust_bill.custnum
5228 #agent virtualization
5229 my $curuser = $FS::CurrentUser::CurrentUser;
5230 if ( $curuser->username eq 'fs_queue'
5231 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5233 my $newuser = qsearchs('access_user', {
5234 'username' => $username,
5238 $curuser = $newuser;
5240 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5243 push @search, $curuser->agentnums_sql;
5245 join(' AND ', @search );
5257 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5258 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base