4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
147 =item promised_date - customer promised payment date, for collection
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
165 sub cust_linked { $_[0]->cust_main_custnum; }
166 sub cust_unlinked_msg {
168 "WARNING: can't find cust_main.custnum ". $self->custnum.
169 ' (cust_bill.invnum '. $self->invnum. ')';
174 Adds this invoice to the database ("Posts" the invoice). If there is an error,
175 returns the error, otherwise returns false.
181 warn "$me insert called\n" if $DEBUG;
183 local $SIG{HUP} = 'IGNORE';
184 local $SIG{INT} = 'IGNORE';
185 local $SIG{QUIT} = 'IGNORE';
186 local $SIG{TERM} = 'IGNORE';
187 local $SIG{TSTP} = 'IGNORE';
188 local $SIG{PIPE} = 'IGNORE';
190 my $oldAutoCommit = $FS::UID::AutoCommit;
191 local $FS::UID::AutoCommit = 0;
194 my $error = $self->SUPER::insert;
196 $dbh->rollback if $oldAutoCommit;
200 if ( $self->get('cust_bill_pkg') ) {
201 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202 $cust_bill_pkg->invnum($self->invnum);
203 my $error = $cust_bill_pkg->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "can't create invoice line item: $error";
211 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 This method now works but you probably shouldn't use it. Instead, apply a
219 credit against the invoice.
221 Using this method to delete invoices outright is really, really bad. There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
225 Really, don't use it.
231 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
256 foreach my $linked ( $self->$table() ) {
257 my $error = $linked->delete;
259 $dbh->rollback if $oldAutoCommit;
266 my $error = $self->SUPER::delete(@_);
268 $dbh->rollback if $oldAutoCommit;
272 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 =item replace [ OLD_RECORD ]
280 You can, but probably shouldn't modify invoices...
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record. If there is an error, returns the error,
284 otherwise returns false.
288 #replace can be inherited from Record.pm
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
294 my( $new, $old ) = ( shift, shift );
295 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296 #return "Can't change _date!" unless $old->_date eq $new->_date;
297 return "Can't change _date" unless $old->_date == $new->_date;
298 return "Can't change charged" unless $old->charged == $new->charged
299 || $old->charged == 0
300 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
306 =item add_cc_surcharge
312 sub add_cc_surcharge {
313 my ($self, $pkgnum, $amount) = (shift, shift, shift);
316 my $cust_bill_pkg = new FS::cust_bill_pkg({
317 'invnum' => $self->invnum,
321 $error = $cust_bill_pkg->insert;
322 return $error if $error;
324 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325 $self->charged($self->charged+$amount);
326 $error = $self->replace;
327 return $error if $error;
329 $self->apply_payments_and_credits;
335 Checks all fields to make sure this is a valid invoice. If there is an error,
336 returns the error, otherwise returns false. Called by the insert and replace
345 $self->ut_numbern('invnum')
346 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347 || $self->ut_numbern('_date')
348 || $self->ut_money('charged')
349 || $self->ut_numbern('printed')
350 || $self->ut_enum('closed', [ '', 'Y' ])
351 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352 || $self->ut_numbern('agent_invid') #varchar?
354 return $error if $error;
356 $self->_date(time) unless $self->_date;
358 $self->printed(0) if $self->printed eq '';
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
372 my $conf = $self->conf;
373 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374 return $self->agent_invid;
376 return $self->invnum;
382 Returns a list consisting of the total previous balance for this customer,
383 followed by the previous outstanding invoices (as FS::cust_bill objects also).
390 my @cust_bill = sort { $a->_date <=> $b->_date }
391 grep { $_->owed != 0 && $_->_date < $self->_date }
392 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
394 foreach ( @cust_bill ) { $total += $_->owed; }
400 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
407 { 'table' => 'cust_bill_pkg',
408 'hashref' => { 'invnum' => $self->invnum },
409 'order_by' => 'ORDER BY billpkgnum',
414 =item cust_bill_pkg_pkgnum PKGNUM
416 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
421 sub cust_bill_pkg_pkgnum {
422 my( $self, $pkgnum ) = @_;
424 { 'table' => 'cust_bill_pkg',
425 'hashref' => { 'invnum' => $self->invnum,
428 'order_by' => 'ORDER BY billpkgnum',
435 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
442 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
443 $self->cust_bill_pkg;
445 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
450 Returns true if any of the packages (or their definitions) corresponding to the
451 line items for this invoice have the no_auto flag set.
457 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
460 =item open_cust_bill_pkg
462 Returns the open line items for this invoice.
464 Note that cust_bill_pkg with both setup and recur fees are returned as two
465 separate line items, each with only one fee.
469 # modeled after cust_main::open_cust_bill
470 sub open_cust_bill_pkg {
473 # grep { $_->owed > 0 } $self->cust_bill_pkg
475 my %other = ( 'recur' => 'setup',
476 'setup' => 'recur', );
478 foreach my $field ( qw( recur setup )) {
479 push @open, map { $_->set( $other{$field}, 0 ); $_; }
480 grep { $_->owed($field) > 0 }
481 $self->cust_bill_pkg;
487 =item cust_bill_event
489 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
493 sub cust_bill_event {
495 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
498 =item num_cust_bill_event
500 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
504 sub num_cust_bill_event {
507 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
508 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
509 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
510 $sth->fetchrow_arrayref->[0];
515 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
519 #false laziness w/cust_pkg.pm
523 'table' => 'cust_event',
524 'addl_from' => 'JOIN part_event USING ( eventpart )',
525 'hashref' => { 'tablenum' => $self->invnum },
526 'extra_sql' => " AND eventtable = 'cust_bill' ",
532 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
536 #false laziness w/cust_pkg.pm
540 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
541 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
542 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
543 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
544 $sth->fetchrow_arrayref->[0];
549 Returns the customer (see L<FS::cust_main>) for this invoice.
555 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
558 =item cust_suspend_if_balance_over AMOUNT
560 Suspends the customer associated with this invoice if the total amount owed on
561 this invoice and all older invoices is greater than the specified amount.
563 Returns a list: an empty list on success or a list of errors.
567 sub cust_suspend_if_balance_over {
568 my( $self, $amount ) = ( shift, shift );
569 my $cust_main = $self->cust_main;
570 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
573 $cust_main->suspend(@_);
579 Depreciated. See the cust_credited method.
581 #Returns a list consisting of the total previous credited (see
582 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
583 #outstanding credits (FS::cust_credit objects).
589 croak "FS::cust_bill->cust_credit depreciated; see ".
590 "FS::cust_bill->cust_credit_bill";
593 #my @cust_credit = sort { $a->_date <=> $b->_date }
594 # grep { $_->credited != 0 && $_->_date < $self->_date }
595 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
597 #foreach (@cust_credit) { $total += $_->credited; }
598 #$total, @cust_credit;
603 Depreciated. See the cust_bill_pay method.
605 #Returns all payments (see L<FS::cust_pay>) for this invoice.
611 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
613 #sort { $a->_date <=> $b->_date }
614 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
620 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
623 sub cust_bill_pay_batch {
625 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
630 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
636 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
637 sort { $a->_date <=> $b->_date }
638 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
643 =item cust_credit_bill
645 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
651 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
652 sort { $a->_date <=> $b->_date }
653 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
657 sub cust_credit_bill {
658 shift->cust_credited(@_);
661 #=item cust_bill_pay_pkgnum PKGNUM
663 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
664 #with matching pkgnum.
668 #sub cust_bill_pay_pkgnum {
669 # my( $self, $pkgnum ) = @_;
670 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
671 # sort { $a->_date <=> $b->_date }
672 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
673 # 'pkgnum' => $pkgnum,
678 =item cust_bill_pay_pkg PKGNUM
680 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
681 applied against the matching pkgnum.
685 sub cust_bill_pay_pkg {
686 my( $self, $pkgnum ) = @_;
689 'select' => 'cust_bill_pay_pkg.*',
690 'table' => 'cust_bill_pay_pkg',
691 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
692 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
693 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
694 " AND cust_bill_pkg.pkgnum = $pkgnum",
699 #=item cust_credited_pkgnum PKGNUM
701 #=item cust_credit_bill_pkgnum PKGNUM
703 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
704 #with matching pkgnum.
708 #sub cust_credited_pkgnum {
709 # my( $self, $pkgnum ) = @_;
710 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
711 # sort { $a->_date <=> $b->_date }
712 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
713 # 'pkgnum' => $pkgnum,
718 #sub cust_credit_bill_pkgnum {
719 # shift->cust_credited_pkgnum(@_);
722 =item cust_credit_bill_pkg PKGNUM
724 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
725 applied against the matching pkgnum.
729 sub cust_credit_bill_pkg {
730 my( $self, $pkgnum ) = @_;
733 'select' => 'cust_credit_bill_pkg.*',
734 'table' => 'cust_credit_bill_pkg',
735 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
736 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
737 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
738 " AND cust_bill_pkg.pkgnum = $pkgnum",
743 =item cust_bill_batch
745 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
749 sub cust_bill_batch {
751 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
756 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
757 hash keyed by term length.
763 FS::discount_plan->all($self);
768 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
775 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
777 foreach (@taxlines) { $total += $_->setup; }
783 Returns the amount owed (still outstanding) on this invoice, which is charged
784 minus all payment applications (see L<FS::cust_bill_pay>) and credit
785 applications (see L<FS::cust_credit_bill>).
791 my $balance = $self->charged;
792 $balance -= $_->amount foreach ( $self->cust_bill_pay );
793 $balance -= $_->amount foreach ( $self->cust_credited );
794 $balance = sprintf( "%.2f", $balance);
795 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
800 my( $self, $pkgnum ) = @_;
802 #my $balance = $self->charged;
804 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
806 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
807 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
809 $balance = sprintf( "%.2f", $balance);
810 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
816 Returns true if this invoice should be hidden. See the
817 selfservice-hide_invoices-taxclass configuraiton setting.
823 my $conf = $self->conf;
824 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
826 my @cust_bill_pkg = $self->cust_bill_pkg;
827 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
828 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
831 =item apply_payments_and_credits [ OPTION => VALUE ... ]
833 Applies unapplied payments and credits to this invoice.
835 A hash of optional arguments may be passed. Currently "manual" is supported.
836 If true, a payment receipt is sent instead of a statement when
837 'payment_receipt_email' configuration option is set.
839 If there is an error, returns the error, otherwise returns false.
843 sub apply_payments_and_credits {
844 my( $self, %options ) = @_;
845 my $conf = $self->conf;
847 local $SIG{HUP} = 'IGNORE';
848 local $SIG{INT} = 'IGNORE';
849 local $SIG{QUIT} = 'IGNORE';
850 local $SIG{TERM} = 'IGNORE';
851 local $SIG{TSTP} = 'IGNORE';
852 local $SIG{PIPE} = 'IGNORE';
854 my $oldAutoCommit = $FS::UID::AutoCommit;
855 local $FS::UID::AutoCommit = 0;
858 $self->select_for_update; #mutex
860 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
861 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
863 if ( $conf->exists('pkg-balances') ) {
864 # limit @payments & @credits to those w/ a pkgnum grepped from $self
865 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
866 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
867 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
870 while ( $self->owed > 0 and ( @payments || @credits ) ) {
873 if ( @payments && @credits ) {
875 #decide which goes first by weight of top (unapplied) line item
877 my @open_lineitems = $self->open_cust_bill_pkg;
880 max( map { $_->part_pkg->pay_weight || 0 }
885 my $max_credit_weight =
886 max( map { $_->part_pkg->credit_weight || 0 }
892 #if both are the same... payments first? it has to be something
893 if ( $max_pay_weight >= $max_credit_weight ) {
899 } elsif ( @payments ) {
901 } elsif ( @credits ) {
904 die "guru meditation #12 and 35";
908 if ( $app eq 'pay' ) {
910 my $payment = shift @payments;
911 $unapp_amount = $payment->unapplied;
912 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
913 $app->pkgnum( $payment->pkgnum )
914 if $conf->exists('pkg-balances') && $payment->pkgnum;
916 } elsif ( $app eq 'credit' ) {
918 my $credit = shift @credits;
919 $unapp_amount = $credit->credited;
920 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
921 $app->pkgnum( $credit->pkgnum )
922 if $conf->exists('pkg-balances') && $credit->pkgnum;
925 die "guru meditation #12 and 35";
929 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
930 warn "owed_pkgnum ". $app->pkgnum;
931 $owed = $self->owed_pkgnum($app->pkgnum);
935 next unless $owed > 0;
937 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
938 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
940 $app->invnum( $self->invnum );
942 my $error = $app->insert(%options);
944 $dbh->rollback if $oldAutoCommit;
945 return "Error inserting ". $app->table. " record: $error";
947 die $error if $error;
951 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
956 =item generate_email OPTION => VALUE ...
964 sender address, required
968 alternate template name, optional
972 text attachment arrayref, optional
976 email subject, optional
980 notice name instead of "Invoice", optional
984 Returns an argument list to be passed to L<FS::Misc::send_email>.
994 my $conf = $self->conf;
996 my $me = '[FS::cust_bill::generate_email]';
999 'from' => $args{'from'},
1000 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1004 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1005 'template' => $args{'template'},
1006 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1007 'no_coupon' => $args{'no_coupon'},
1010 my $cust_main = $self->cust_main;
1012 if (ref($args{'to'}) eq 'ARRAY') {
1013 $return{'to'} = $args{'to'};
1015 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1016 $cust_main->invoicing_list
1020 if ( $conf->exists('invoice_html') ) {
1022 warn "$me creating HTML/text multipart message"
1025 $return{'nobody'} = 1;
1027 my $alternative = build MIME::Entity
1028 'Type' => 'multipart/alternative',
1029 #'Encoding' => '7bit',
1030 'Disposition' => 'inline'
1034 if ( $conf->exists('invoice_email_pdf')
1035 and scalar($conf->config('invoice_email_pdf_note')) ) {
1037 warn "$me using 'invoice_email_pdf_note' in multipart message"
1039 $data = [ map { $_ . "\n" }
1040 $conf->config('invoice_email_pdf_note')
1045 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1047 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1048 $data = $args{'print_text'};
1050 $data = [ $self->print_text(\%opt) ];
1055 $alternative->attach(
1056 'Type' => 'text/plain',
1057 'Encoding' => 'quoted-printable',
1058 #'Encoding' => '7bit',
1060 'Disposition' => 'inline',
1067 if ( $conf->exists('invoice_email_pdf')
1068 and scalar($conf->config('invoice_email_pdf_note')) ) {
1070 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1074 $args{'from'} =~ /\@([\w\.\-]+)/;
1075 my $from = $1 || 'example.com';
1076 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1079 my $agentnum = $cust_main->agentnum;
1080 if ( defined($args{'template'}) && length($args{'template'})
1081 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1084 $logo = 'logo_'. $args{'template'}. '.png';
1088 my $image_data = $conf->config_binary( $logo, $agentnum);
1090 $image = build MIME::Entity
1091 'Type' => 'image/png',
1092 'Encoding' => 'base64',
1093 'Data' => $image_data,
1094 'Filename' => 'logo.png',
1095 'Content-ID' => "<$content_id>",
1098 if ($conf->exists('invoice-barcode')) {
1099 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1100 $barcode = build MIME::Entity
1101 'Type' => 'image/png',
1102 'Encoding' => 'base64',
1103 'Data' => $self->invoice_barcode(0),
1104 'Filename' => 'barcode.png',
1105 'Content-ID' => "<$barcode_content_id>",
1107 $opt{'barcode_cid'} = $barcode_content_id;
1110 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1113 $alternative->attach(
1114 'Type' => 'text/html',
1115 'Encoding' => 'quoted-printable',
1116 'Data' => [ '<html>',
1119 ' '. encode_entities($return{'subject'}),
1122 ' <body bgcolor="#e8e8e8">',
1127 'Disposition' => 'inline',
1128 #'Filename' => 'invoice.pdf',
1132 my @otherparts = ();
1133 if ( $cust_main->email_csv_cdr ) {
1135 push @otherparts, build MIME::Entity
1136 'Type' => 'text/csv',
1137 'Encoding' => '7bit',
1138 'Data' => [ map { "$_\n" }
1139 $self->call_details('prepend_billed_number' => 1)
1141 'Disposition' => 'attachment',
1142 'Filename' => 'usage-'. $self->invnum. '.csv',
1147 if ( $conf->exists('invoice_email_pdf') ) {
1152 # multipart/alternative
1158 my $related = build MIME::Entity 'Type' => 'multipart/related',
1159 'Encoding' => '7bit';
1161 #false laziness w/Misc::send_email
1162 $related->head->replace('Content-type',
1163 $related->mime_type.
1164 '; boundary="'. $related->head->multipart_boundary. '"'.
1165 '; type=multipart/alternative'
1168 $related->add_part($alternative);
1170 $related->add_part($image) if $image;
1172 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1174 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1178 #no other attachment:
1180 # multipart/alternative
1185 $return{'content-type'} = 'multipart/related';
1186 if ($conf->exists('invoice-barcode') && $barcode) {
1187 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1189 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1191 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1192 #$return{'disposition'} = 'inline';
1198 if ( $conf->exists('invoice_email_pdf') ) {
1199 warn "$me creating PDF attachment"
1202 #mime parts arguments a la MIME::Entity->build().
1203 $return{'mimeparts'} = [
1204 { $self->mimebuild_pdf(\%opt) }
1208 if ( $conf->exists('invoice_email_pdf')
1209 and scalar($conf->config('invoice_email_pdf_note')) ) {
1211 warn "$me using 'invoice_email_pdf_note'"
1213 $return{'body'} = [ map { $_ . "\n" }
1214 $conf->config('invoice_email_pdf_note')
1219 warn "$me not using 'invoice_email_pdf_note'"
1221 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1222 $return{'body'} = $args{'print_text'};
1224 $return{'body'} = [ $self->print_text(\%opt) ];
1237 Returns a list suitable for passing to MIME::Entity->build(), representing
1238 this invoice as PDF attachment.
1245 'Type' => 'application/pdf',
1246 'Encoding' => 'base64',
1247 'Data' => [ $self->print_pdf(@_) ],
1248 'Disposition' => 'attachment',
1249 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1253 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1255 Sends this invoice to the destinations configured for this customer: sends
1256 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1258 Options can be passed as a hashref (recommended) or as a list of up to
1259 four values for templatename, agentnum, invoice_from and amount.
1261 I<template>, if specified, is the name of a suffix for alternate invoices.
1263 I<agentnum>, if specified, means that this invoice will only be sent for customers
1264 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1265 single agent) or an arrayref of agentnums.
1267 I<invoice_from>, if specified, overrides the default email invoice From: address.
1269 I<amount>, if specified, only sends the invoice if the total amount owed on this
1270 invoice and all older invoices is greater than the specified amount.
1272 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1276 sub queueable_send {
1279 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1280 or die "invalid invoice number: " . $opt{invnum};
1282 my @args = ( $opt{template}, $opt{agentnum} );
1283 push @args, $opt{invoice_from}
1284 if exists($opt{invoice_from}) && $opt{invoice_from};
1286 my $error = $self->send( @args );
1287 die $error if $error;
1293 my $conf = $self->conf;
1295 my( $template, $invoice_from, $notice_name );
1297 my $balance_over = 0;
1301 $template = $opt->{'template'} || '';
1302 if ( $agentnums = $opt->{'agentnum'} ) {
1303 $agentnums = [ $agentnums ] unless ref($agentnums);
1305 $invoice_from = $opt->{'invoice_from'};
1306 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1307 $notice_name = $opt->{'notice_name'};
1309 $template = scalar(@_) ? shift : '';
1310 if ( scalar(@_) && $_[0] ) {
1311 $agentnums = ref($_[0]) ? shift : [ shift ];
1313 $invoice_from = shift if scalar(@_);
1314 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1317 return 'N/A' unless ! $agentnums
1318 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1321 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1323 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1324 $conf->config('invoice_from', $self->cust_main->agentnum );
1327 'template' => $template,
1328 'invoice_from' => $invoice_from,
1329 'notice_name' => ( $notice_name || 'Invoice' ),
1332 my @invoicing_list = $self->cust_main->invoicing_list;
1334 #$self->email_invoice(\%opt)
1336 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1338 #$self->print_invoice(\%opt)
1340 if grep { $_ eq 'POST' } @invoicing_list; #postal
1342 $self->fax_invoice(\%opt)
1343 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1349 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1351 Emails this invoice.
1353 Options can be passed as a hashref (recommended) or as a list of up to
1354 two values for templatename and invoice_from.
1356 I<template>, if specified, is the name of a suffix for alternate invoices.
1358 I<invoice_from>, if specified, overrides the default email invoice From: address.
1360 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1364 sub queueable_email {
1367 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1368 or die "invalid invoice number: " . $opt{invnum};
1370 my %args = ( 'template' => $opt{template} );
1371 $args{$_} = $opt{$_}
1372 foreach grep { exists($opt{$_}) && $opt{$_} }
1373 qw( invoice_from notice_name no_coupon );
1375 my $error = $self->email( \%args );
1376 die $error if $error;
1380 #sub email_invoice {
1383 return if $self->hide;
1384 my $conf = $self->conf;
1386 my( $template, $invoice_from, $notice_name, $no_coupon );
1389 $template = $opt->{'template'} || '';
1390 $invoice_from = $opt->{'invoice_from'};
1391 $notice_name = $opt->{'notice_name'} || 'Invoice';
1392 $no_coupon = $opt->{'no_coupon'} || 0;
1394 $template = scalar(@_) ? shift : '';
1395 $invoice_from = shift if scalar(@_);
1396 $notice_name = 'Invoice';
1400 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1401 $conf->config('invoice_from', $self->cust_main->agentnum );
1403 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1404 $self->cust_main->invoicing_list;
1406 if ( ! @invoicing_list ) { #no recipients
1407 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1408 die 'No recipients for customer #'. $self->custnum;
1410 #default: better to notify this person than silence
1411 @invoicing_list = ($invoice_from);
1415 my $subject = $self->email_subject($template);
1417 my $error = send_email(
1418 $self->generate_email(
1419 'from' => $invoice_from,
1420 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1421 'subject' => $subject,
1422 'template' => $template,
1423 'notice_name' => $notice_name,
1424 'no_coupon' => $no_coupon,
1427 die "can't email invoice: $error\n" if $error;
1428 #die "$error\n" if $error;
1434 my $conf = $self->conf;
1436 #my $template = scalar(@_) ? shift : '';
1439 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1442 my $cust_main = $self->cust_main;
1443 my $name = $cust_main->name;
1444 my $name_short = $cust_main->name_short;
1445 my $invoice_number = $self->invnum;
1446 my $invoice_date = $self->_date_pretty;
1448 eval qq("$subject");
1451 =item lpr_data HASHREF | [ TEMPLATE ]
1453 Returns the postscript or plaintext for this invoice as an arrayref.
1455 Options can be passed as a hashref (recommended) or as a single optional value
1458 I<template>, if specified, is the name of a suffix for alternate invoices.
1460 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1466 my $conf = $self->conf;
1467 my( $template, $notice_name );
1470 $template = $opt->{'template'} || '';
1471 $notice_name = $opt->{'notice_name'} || 'Invoice';
1473 $template = scalar(@_) ? shift : '';
1474 $notice_name = 'Invoice';
1478 'template' => $template,
1479 'notice_name' => $notice_name,
1482 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1483 [ $self->$method( \%opt ) ];
1486 =item print HASHREF | [ TEMPLATE ]
1488 Prints this invoice.
1490 Options can be passed as a hashref (recommended) or as a single optional
1493 I<template>, if specified, is the name of a suffix for alternate invoices.
1495 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1499 #sub print_invoice {
1502 return if $self->hide;
1503 my $conf = $self->conf;
1505 my( $template, $notice_name );
1508 $template = $opt->{'template'} || '';
1509 $notice_name = $opt->{'notice_name'} || 'Invoice';
1511 $template = scalar(@_) ? shift : '';
1512 $notice_name = 'Invoice';
1516 'template' => $template,
1517 'notice_name' => $notice_name,
1520 if($conf->exists('invoice_print_pdf')) {
1521 # Add the invoice to the current batch.
1522 $self->batch_invoice(\%opt);
1525 do_print $self->lpr_data(\%opt);
1529 =item fax_invoice HASHREF | [ TEMPLATE ]
1533 Options can be passed as a hashref (recommended) or as a single optional
1536 I<template>, if specified, is the name of a suffix for alternate invoices.
1538 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1544 return if $self->hide;
1545 my $conf = $self->conf;
1547 my( $template, $notice_name );
1550 $template = $opt->{'template'} || '';
1551 $notice_name = $opt->{'notice_name'} || 'Invoice';
1553 $template = scalar(@_) ? shift : '';
1554 $notice_name = 'Invoice';
1557 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1558 unless $conf->exists('invoice_latex');
1560 my $dialstring = $self->cust_main->getfield('fax');
1564 'template' => $template,
1565 'notice_name' => $notice_name,
1568 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1569 'dialstring' => $dialstring,
1571 die $error if $error;
1575 =item batch_invoice [ HASHREF ]
1577 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1578 isn't an open batch, one will be created.
1583 my ($self, $opt) = @_;
1584 my $bill_batch = $self->get_open_bill_batch;
1585 my $cust_bill_batch = FS::cust_bill_batch->new({
1586 batchnum => $bill_batch->batchnum,
1587 invnum => $self->invnum,
1589 return $cust_bill_batch->insert($opt);
1592 =item get_open_batch
1594 Returns the currently open batch as an FS::bill_batch object, creating a new
1595 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1600 sub get_open_bill_batch {
1602 my $conf = $self->conf;
1603 my $hashref = { status => 'O' };
1604 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1605 ? $self->cust_main->agentnum
1607 my $batch = qsearchs('bill_batch', $hashref);
1608 return $batch if $batch;
1609 $batch = FS::bill_batch->new($hashref);
1610 my $error = $batch->insert;
1611 die $error if $error;
1615 =item ftp_invoice [ TEMPLATENAME ]
1617 Sends this invoice data via FTP.
1619 TEMPLATENAME is unused?
1625 my $conf = $self->conf;
1626 my $template = scalar(@_) ? shift : '';
1629 'protocol' => 'ftp',
1630 'server' => $conf->config('cust_bill-ftpserver'),
1631 'username' => $conf->config('cust_bill-ftpusername'),
1632 'password' => $conf->config('cust_bill-ftppassword'),
1633 'dir' => $conf->config('cust_bill-ftpdir'),
1634 'format' => $conf->config('cust_bill-ftpformat'),
1638 =item spool_invoice [ TEMPLATENAME ]
1640 Spools this invoice data (see L<FS::spool_csv>)
1642 TEMPLATENAME is unused?
1648 my $conf = $self->conf;
1649 my $template = scalar(@_) ? shift : '';
1652 'format' => $conf->config('cust_bill-spoolformat'),
1653 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1657 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1659 Like B<send>, but only sends the invoice if it is the newest open invoice for
1664 sub send_if_newest {
1669 grep { $_->owed > 0 }
1670 qsearch('cust_bill', {
1671 'custnum' => $self->custnum,
1672 #'_date' => { op=>'>', value=>$self->_date },
1673 'invnum' => { op=>'>', value=>$self->invnum },
1680 =item send_csv OPTION => VALUE, ...
1682 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1686 protocol - currently only "ftp"
1692 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1693 and YYMMDDHHMMSS is a timestamp.
1695 See L</print_csv> for a description of the output format.
1700 my($self, %opt) = @_;
1704 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1705 mkdir $spooldir, 0700 unless -d $spooldir;
1707 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1708 my $file = "$spooldir/$tracctnum.csv";
1710 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1712 open(CSV, ">$file") or die "can't open $file: $!";
1720 if ( $opt{protocol} eq 'ftp' ) {
1721 eval "use Net::FTP;";
1723 $net = Net::FTP->new($opt{server}) or die @$;
1725 die "unknown protocol: $opt{protocol}";
1728 $net->login( $opt{username}, $opt{password} )
1729 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1731 $net->binary or die "can't set binary mode";
1733 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1735 $net->put($file) or die "can't put $file: $!";
1745 Spools CSV invoice data.
1751 =item format - 'default' or 'billco'
1753 =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>).
1755 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1757 =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.
1764 my($self, %opt) = @_;
1766 my $cust_main = $self->cust_main;
1768 if ( $opt{'dest'} ) {
1769 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1770 $cust_main->invoicing_list;
1771 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1772 || ! keys %invoicing_list;
1775 if ( $opt{'balanceover'} ) {
1777 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1780 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1781 mkdir $spooldir, 0700 unless -d $spooldir;
1783 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1787 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1788 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1791 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1793 open(CSV, ">>$file") or die "can't open $file: $!";
1794 flock(CSV, LOCK_EX);
1799 if ( lc($opt{'format'}) eq 'billco' ) {
1801 flock(CSV, LOCK_UN);
1806 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1809 open(CSV,">>$file") or die "can't open $file: $!";
1810 flock(CSV, LOCK_EX);
1816 flock(CSV, LOCK_UN);
1823 =item print_csv OPTION => VALUE, ...
1825 Returns CSV data for this invoice.
1829 format - 'default' or 'billco'
1831 Returns a list consisting of two scalars. The first is a single line of CSV
1832 header information for this invoice. The second is one or more lines of CSV
1833 detail information for this invoice.
1835 If I<format> is not specified or "default", the fields of the CSV file are as
1838 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1842 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1844 B<record_type> is C<cust_bill> for the initial header line only. The
1845 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1846 fields are filled in.
1848 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1849 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1852 =item invnum - invoice number
1854 =item custnum - customer number
1856 =item _date - invoice date
1858 =item charged - total invoice amount
1860 =item first - customer first name
1862 =item last - customer first name
1864 =item company - company name
1866 =item address1 - address line 1
1868 =item address2 - address line 1
1878 =item pkg - line item description
1880 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1882 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1884 =item sdate - start date for recurring fee
1886 =item edate - end date for recurring fee
1890 If I<format> is "billco", the fields of the header CSV file are as follows:
1892 +-------------------------------------------------------------------+
1893 | FORMAT HEADER FILE |
1894 |-------------------------------------------------------------------|
1895 | Field | Description | Name | Type | Width |
1896 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1897 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1898 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1899 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1900 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1901 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1902 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1903 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1904 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1905 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1906 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1907 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1908 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1909 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1910 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1911 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1912 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1913 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1914 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1915 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1916 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1917 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1918 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1919 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1920 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1921 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1922 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1923 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1924 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1925 +-------+-------------------------------+------------+------+-------+
1927 If I<format> is "billco", the fields of the detail CSV file are as follows:
1929 FORMAT FOR DETAIL FILE
1931 Field | Description | Name | Type | Width
1932 1 | N/A-Leave Empty | RC | CHAR | 2
1933 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1934 3 | Account Number | TRACCTNUM | CHAR | 15
1935 4 | Invoice Number | TRINVOICE | CHAR | 15
1936 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1937 6 | Transaction Detail | DETAILS | CHAR | 100
1938 7 | Amount | AMT | NUM* | 9
1939 8 | Line Format Control** | LNCTRL | CHAR | 2
1940 9 | Grouping Code | GROUP | CHAR | 2
1941 10 | User Defined | ACCT CODE | CHAR | 15
1946 my($self, %opt) = @_;
1948 eval "use Text::CSV_XS";
1951 my $cust_main = $self->cust_main;
1953 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1955 if ( lc($opt{'format'}) eq 'billco' ) {
1958 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1960 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1962 my( $previous_balance, @unused ) = $self->previous; #previous balance
1964 my $pmt_cr_applied = 0;
1965 $pmt_cr_applied += $_->{'amount'}
1966 foreach ( $self->_items_payments, $self->_items_credits ) ;
1968 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1971 '', # 1 | N/A-Leave Empty CHAR 2
1972 '', # 2 | N/A-Leave Empty CHAR 15
1973 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1974 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1975 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1976 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1977 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1978 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1979 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1980 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1981 '', # 10 | Ancillary Billing Information CHAR 30
1982 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1983 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1986 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1989 $duedate, # 14 | Bill Due Date CHAR 10
1991 $previous_balance, # 15 | Previous Balance NUM* 9
1992 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1993 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1994 $totaldue, # 18 | Total Amt Due NUM* 9
1995 $totaldue, # 19 | Total Amt Due NUM* 9
1996 '', # 20 | 30 Day Aging NUM* 9
1997 '', # 21 | 60 Day Aging NUM* 9
1998 '', # 22 | 90 Day Aging NUM* 9
1999 'N', # 23 | Y/N CHAR 1
2000 '', # 24 | Remittance automation CHAR 100
2001 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2002 $self->custnum, # 26 | Customer Reference Number CHAR 15
2003 '0', # 27 | Federal Tax*** NUM* 9
2004 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2005 '0', # 29 | Other Taxes & Fees*** NUM* 9
2008 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2010 my ($previous_balance) = $self->previous;
2011 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2013 ($_->{pkgnum} || ''),
2016 } $self->_items_pkg;
2019 $cust_main->agentnum,
2020 $cust_main->agent->agent,
2024 $cust_main->address1,
2025 $cust_main->address2,
2031 time2str("%x", $self->_date),
2045 time2str("%x", $self->_date),
2046 sprintf("%.2f", $self->charged),
2047 ( map { $cust_main->getfield($_) }
2048 qw( first last company address1 address2 city state zip country ) ),
2050 ) or die "can't create csv";
2053 my $header = $csv->string. "\n";
2056 if ( lc($opt{'format'}) eq 'billco' ) {
2059 foreach my $item ( $self->_items_pkg ) {
2062 '', # 1 | N/A-Leave Empty CHAR 2
2063 '', # 2 | N/A-Leave Empty CHAR 15
2064 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2065 $self->invnum, # 4 | Invoice Number CHAR 15
2066 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2067 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2068 $item->{'amount'}, # 7 | Amount NUM* 9
2069 '', # 8 | Line Format Control** CHAR 2
2070 '', # 9 | Grouping Code CHAR 2
2071 '', # 10 | User Defined CHAR 15
2074 $detail .= $csv->string. "\n";
2078 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2084 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2086 my($pkg, $setup, $recur, $sdate, $edate);
2087 if ( $cust_bill_pkg->pkgnum ) {
2089 ($pkg, $setup, $recur, $sdate, $edate) = (
2090 $cust_bill_pkg->part_pkg->pkg,
2091 ( $cust_bill_pkg->setup != 0
2092 ? sprintf("%.2f", $cust_bill_pkg->setup )
2094 ( $cust_bill_pkg->recur != 0
2095 ? sprintf("%.2f", $cust_bill_pkg->recur )
2097 ( $cust_bill_pkg->sdate
2098 ? time2str("%x", $cust_bill_pkg->sdate)
2100 ($cust_bill_pkg->edate
2101 ?time2str("%x", $cust_bill_pkg->edate)
2105 } else { #pkgnum tax
2106 next unless $cust_bill_pkg->setup != 0;
2107 $pkg = $cust_bill_pkg->desc;
2108 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2109 ( $sdate, $edate ) = ( '', '' );
2115 ( map { '' } (1..11) ),
2116 ($pkg, $setup, $recur, $sdate, $edate)
2117 ) or die "can't create csv";
2119 $detail .= $csv->string. "\n";
2125 ( $header, $detail );
2131 Pays this invoice with a compliemntary payment. If there is an error,
2132 returns the error, otherwise returns false.
2138 my $cust_pay = new FS::cust_pay ( {
2139 'invnum' => $self->invnum,
2140 'paid' => $self->owed,
2143 'payinfo' => $self->cust_main->payinfo,
2151 Attempts to pay this invoice with a credit card payment via a
2152 Business::OnlinePayment realtime gateway. See
2153 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2154 for supported processors.
2160 $self->realtime_bop( 'CC', @_ );
2165 Attempts to pay this invoice with an electronic check (ACH) payment via a
2166 Business::OnlinePayment realtime gateway. See
2167 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2168 for supported processors.
2174 $self->realtime_bop( 'ECHECK', @_ );
2179 Attempts to pay this invoice with phone bill (LEC) payment via a
2180 Business::OnlinePayment realtime gateway. See
2181 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2182 for supported processors.
2188 $self->realtime_bop( 'LEC', @_ );
2192 my( $self, $method ) = (shift,shift);
2193 my $conf = $self->conf;
2196 my $cust_main = $self->cust_main;
2197 my $balance = $cust_main->balance;
2198 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2199 $amount = sprintf("%.2f", $amount);
2200 return "not run (balance $balance)" unless $amount > 0;
2202 my $description = 'Internet Services';
2203 if ( $conf->exists('business-onlinepayment-description') ) {
2204 my $dtempl = $conf->config('business-onlinepayment-description');
2206 my $agent_obj = $cust_main->agent
2207 or die "can't retreive agent for $cust_main (agentnum ".
2208 $cust_main->agentnum. ")";
2209 my $agent = $agent_obj->agent;
2210 my $pkgs = join(', ',
2211 map { $_->part_pkg->pkg }
2212 grep { $_->pkgnum } $self->cust_bill_pkg
2214 $description = eval qq("$dtempl");
2217 $cust_main->realtime_bop($method, $amount,
2218 'description' => $description,
2219 'invnum' => $self->invnum,
2220 #this didn't do what we want, it just calls apply_payments_and_credits
2222 'apply_to_invoice' => 1,
2225 #this changes application behavior: auto payments
2226 #triggered against a specific invoice are now applied
2227 #to that invoice instead of oldest open.
2233 =item batch_card OPTION => VALUE...
2235 Adds a payment for this invoice to the pending credit card batch (see
2236 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2237 runs the payment using a realtime gateway.
2242 my ($self, %options) = @_;
2243 my $cust_main = $self->cust_main;
2245 $options{invnum} = $self->invnum;
2247 $cust_main->batch_card(%options);
2250 sub _agent_template {
2252 $self->cust_main->agent_template;
2255 sub _agent_invoice_from {
2257 $self->cust_main->agent_invoice_from;
2260 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2262 Returns an text invoice, as a list of lines.
2264 Options can be passed as a hashref (recommended) or as a list of time, template
2265 and then any key/value pairs for any other options.
2267 I<time>, if specified, is used to control the printing of overdue messages. The
2268 default is now. It isn't the date of the invoice; that's the `_date' field.
2269 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2270 L<Time::Local> and L<Date::Parse> for conversion functions.
2272 I<template>, if specified, is the name of a suffix for alternate invoices.
2274 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2280 my( $today, $template, %opt );
2282 %opt = %{ shift() };
2283 $today = delete($opt{'time'}) || '';
2284 $template = delete($opt{template}) || '';
2286 ( $today, $template, %opt ) = @_;
2289 my %params = ( 'format' => 'template' );
2290 $params{'time'} = $today if $today;
2291 $params{'template'} = $template if $template;
2292 $params{$_} = $opt{$_}
2293 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2295 $self->print_generic( %params );
2298 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2300 Internal method - returns a filename of a filled-in LaTeX template for this
2301 invoice (Note: add ".tex" to get the actual filename), and a filename of
2302 an associated logo (with the .eps extension included).
2304 See print_ps and print_pdf for methods that return PostScript and PDF output.
2306 Options can be passed as a hashref (recommended) or as a list of time, template
2307 and then any key/value pairs for any other options.
2309 I<time>, if specified, is used to control the printing of overdue messages. The
2310 default is now. It isn't the date of the invoice; that's the `_date' field.
2311 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2312 L<Time::Local> and L<Date::Parse> for conversion functions.
2314 I<template>, if specified, is the name of a suffix for alternate invoices.
2316 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2322 my $conf = $self->conf;
2323 my( $today, $template, %opt );
2325 %opt = %{ shift() };
2326 $today = delete($opt{'time'}) || '';
2327 $template = delete($opt{template}) || '';
2329 ( $today, $template, %opt ) = @_;
2332 my %params = ( 'format' => 'latex' );
2333 $params{'time'} = $today if $today;
2334 $params{'template'} = $template if $template;
2335 $params{$_} = $opt{$_}
2336 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2338 $template ||= $self->_agent_template;
2340 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2341 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2345 ) or die "can't open temp file: $!\n";
2347 my $agentnum = $self->cust_main->agentnum;
2349 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2350 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2351 or die "can't write temp file: $!\n";
2353 print $lh $conf->config_binary('logo.eps', $agentnum)
2354 or die "can't write temp file: $!\n";
2357 $params{'logo_file'} = $lh->filename;
2359 if($conf->exists('invoice-barcode')){
2360 my $png_file = $self->invoice_barcode($dir);
2361 my $eps_file = $png_file;
2362 $eps_file =~ s/\.png$/.eps/g;
2363 $png_file =~ /(barcode.*png)/;
2365 $eps_file =~ /(barcode.*eps)/;
2368 my $curr_dir = cwd();
2370 # after painfuly long experimentation, it was determined that sam2p won't
2371 # accept : and other chars in the path, no matter how hard I tried to
2372 # escape them, hence the chdir (and chdir back, just to be safe)
2373 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2374 or die "sam2p failed: $!\n";
2378 $params{'barcode_file'} = $eps_file;
2381 my @filled_in = $self->print_generic( %params );
2383 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2387 ) or die "can't open temp file: $!\n";
2388 binmode($fh, ':utf8'); # language support
2389 print $fh join('', @filled_in );
2392 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2393 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2397 =item invoice_barcode DIR_OR_FALSE
2399 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2400 it is taken as the temp directory where the PNG file will be generated and the
2401 PNG file name is returned. Otherwise, the PNG image itself is returned.
2405 sub invoice_barcode {
2406 my ($self, $dir) = (shift,shift);
2408 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2409 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2410 my $gd = $gdbar->plot(Height => 30);
2413 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2417 ) or die "can't open temp file: $!\n";
2418 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2419 my $png_file = $bh->filename;
2426 =item print_generic OPTION => VALUE ...
2428 Internal method - returns a filled-in template for this invoice as a scalar.
2430 See print_ps and print_pdf for methods that return PostScript and PDF output.
2432 Non optional options include
2433 format - latex, html, template
2435 Optional options include
2437 template - a value used as a suffix for a configuration template
2439 time - a value used to control the printing of overdue messages. The
2440 default is now. It isn't the date of the invoice; that's the `_date' field.
2441 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2442 L<Time::Local> and L<Date::Parse> for conversion functions.
2446 unsquelch_cdr - overrides any per customer cdr squelching when true
2448 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2450 locale - override customer's locale
2454 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2455 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2456 # yes: fixed width/plain text printing will be borked
2458 my( $self, %params ) = @_;
2459 my $conf = $self->conf;
2460 my $today = $params{today} ? $params{today} : time;
2461 warn "$me print_generic called on $self with suffix $params{template}\n"
2464 my $format = $params{format};
2465 die "Unknown format: $format"
2466 unless $format =~ /^(latex|html|template)$/;
2468 my $cust_main = $self->cust_main;
2469 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2470 unless $cust_main->payname
2471 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2473 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2474 'html' => [ '<%=', '%>' ],
2475 'template' => [ '{', '}' ],
2478 warn "$me print_generic creating template\n"
2481 #create the template
2482 my $template = $params{template} ? $params{template} : $self->_agent_template;
2483 my $templatefile = "invoice_$format";
2484 $templatefile .= "_$template"
2485 if length($template) && $conf->exists($templatefile."_$template");
2486 my @invoice_template = map "$_\n", $conf->config($templatefile)
2487 or die "cannot load config data $templatefile";
2490 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2491 #change this to a die when the old code is removed
2492 warn "old-style invoice template $templatefile; ".
2493 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2494 $old_latex = 'true';
2495 @invoice_template = _translate_old_latex_format(@invoice_template);
2498 warn "$me print_generic creating T:T object\n"
2501 my $text_template = new Text::Template(
2503 SOURCE => \@invoice_template,
2504 DELIMITERS => $delimiters{$format},
2507 warn "$me print_generic compiling T:T object\n"
2510 $text_template->compile()
2511 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2514 # additional substitution could possibly cause breakage in existing templates
2515 my %convert_maps = (
2517 'notes' => sub { map "$_", @_ },
2518 'footer' => sub { map "$_", @_ },
2519 'smallfooter' => sub { map "$_", @_ },
2520 'returnaddress' => sub { map "$_", @_ },
2521 'coupon' => sub { map "$_", @_ },
2522 'summary' => sub { map "$_", @_ },
2528 s/%%(.*)$/<!-- $1 -->/g;
2529 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2530 s/\\begin\{enumerate\}/<ol>/g;
2532 s/\\end\{enumerate\}/<\/ol>/g;
2533 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2542 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2544 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2549 s/\\\\\*?\s*$/<BR>/;
2550 s/\\hyphenation\{[\w\s\-]+}//;
2555 'coupon' => sub { "" },
2556 'summary' => sub { "" },
2563 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2564 s/\\begin\{enumerate\}//g;
2566 s/\\end\{enumerate\}//g;
2567 s/\\textbf\{(.*)\}/$1/g;
2574 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2576 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2581 s/\\\\\*?\s*$/\n/; # dubious
2582 s/\\hyphenation\{[\w\s\-]+}//;
2586 'coupon' => sub { "" },
2587 'summary' => sub { "" },
2592 # hashes for differing output formats
2593 my %nbsps = ( 'latex' => '~',
2594 'html' => '', # '&nbps;' would be nice
2595 'template' => '', # not used
2597 my $nbsp = $nbsps{$format};
2599 my %escape_functions = ( 'latex' => \&_latex_escape,
2600 'html' => \&_html_escape_nbsp,#\&encode_entities,
2601 'template' => sub { shift },
2603 my $escape_function = $escape_functions{$format};
2604 my $escape_function_nonbsp = ($format eq 'html')
2605 ? \&_html_escape : $escape_function;
2607 my %date_formats = ( 'latex' => $date_format_long,
2608 'html' => $date_format_long,
2611 $date_formats{'html'} =~ s/ / /g;
2613 my $date_format = $date_formats{$format};
2615 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2617 'html' => sub { return '<b>'. shift(). '</b>'
2619 'template' => sub { shift },
2621 my $embolden_function = $embolden_functions{$format};
2623 my %newline_tokens = ( 'latex' => '\\\\',
2627 my $newline_token = $newline_tokens{$format};
2629 warn "$me generating template variables\n"
2632 # generate template variables
2635 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2639 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2645 $returnaddress = join("\n",
2646 $conf->config_orbase("invoice_${format}returnaddress", $template)
2649 } elsif ( grep /\S/,
2650 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2652 my $convert_map = $convert_maps{$format}{'returnaddress'};
2655 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2660 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2662 my $convert_map = $convert_maps{$format}{'returnaddress'};
2663 $returnaddress = join( "\n", &$convert_map(
2664 map { s/( {2,})/'~' x length($1)/eg;
2668 ( $conf->config('company_name', $self->cust_main->agentnum),
2669 $conf->config('company_address', $self->cust_main->agentnum),
2676 my $warning = "Couldn't find a return address; ".
2677 "do you need to set the company_address configuration value?";
2679 $returnaddress = $nbsp;
2680 #$returnaddress = $warning;
2684 warn "$me generating invoice data\n"
2687 my $agentnum = $self->cust_main->agentnum;
2689 my %invoice_data = (
2692 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2693 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2694 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2695 'returnaddress' => $returnaddress,
2696 'agent' => &$escape_function($cust_main->agent->agent),
2699 'invnum' => $self->invnum,
2700 'date' => time2str($date_format, $self->_date),
2701 'today' => time2str($date_format_long, $today),
2702 'terms' => $self->terms,
2703 'template' => $template, #params{'template'},
2704 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2705 'current_charges' => sprintf("%.2f", $self->charged),
2706 'duedate' => $self->due_date2str($rdate_format), #date_format?
2709 'custnum' => $cust_main->display_custnum,
2710 'agent_custid' => &$escape_function($cust_main->agent_custid),
2711 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2712 payname company address1 address2 city state zip fax
2716 'ship_enable' => $conf->exists('invoice-ship_address'),
2717 'unitprices' => $conf->exists('invoice-unitprice'),
2718 'smallernotes' => $conf->exists('invoice-smallernotes'),
2719 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2720 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2722 #layout info -- would be fancy to calc some of this and bury the template
2724 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2725 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2726 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2727 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2728 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2729 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2730 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2731 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2732 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2733 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2735 # better hang on to conf_dir for a while (for old templates)
2736 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2738 #these are only used when doing paged plaintext
2745 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2746 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2747 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2748 # eval to avoid death for unimplemented languages
2749 my $dh = eval { Date::Language->new($info{'name'}) } ||
2750 Date::Language->new(); # fall back to English
2751 # prototype here to silence warnings
2752 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2753 # eventually use this date handle everywhere in here, too
2755 my $min_sdate = 999999999999;
2757 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2758 next unless $cust_bill_pkg->pkgnum > 0;
2759 $min_sdate = $cust_bill_pkg->sdate
2760 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2761 $max_edate = $cust_bill_pkg->edate
2762 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2765 $invoice_data{'bill_period'} = '';
2766 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2767 . " to " . time2str('%e %h', $max_edate)
2768 if ($max_edate != 0 && $min_sdate != 999999999999);
2770 $invoice_data{finance_section} = '';
2771 if ( $conf->config('finance_pkgclass') ) {
2773 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2774 $invoice_data{finance_section} = $pkg_class->categoryname;
2776 $invoice_data{finance_amount} = '0.00';
2777 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2779 my $countrydefault = $conf->config('countrydefault') || 'US';
2780 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2781 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2782 my $method = $prefix.$_;
2783 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2785 $invoice_data{'ship_country'} = ''
2786 if ( $invoice_data{'ship_country'} eq $countrydefault );
2788 $invoice_data{'cid'} = $params{'cid'}
2791 if ( $cust_main->country eq $countrydefault ) {
2792 $invoice_data{'country'} = '';
2794 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2798 $invoice_data{'address'} = \@address;
2800 $cust_main->payname.
2801 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2802 ? " (P.O. #". $cust_main->payinfo. ")"
2806 push @address, $cust_main->company
2807 if $cust_main->company;
2808 push @address, $cust_main->address1;
2809 push @address, $cust_main->address2
2810 if $cust_main->address2;
2812 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2813 push @address, $invoice_data{'country'}
2814 if $invoice_data{'country'};
2816 while (scalar(@address) < 5);
2818 $invoice_data{'logo_file'} = $params{'logo_file'}
2819 if $params{'logo_file'};
2820 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2821 if $params{'barcode_file'};
2822 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2823 if $params{'barcode_img'};
2824 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2825 if $params{'barcode_cid'};
2827 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2828 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2829 #my $balance_due = $self->owed + $pr_total - $cr_total;
2830 my $balance_due = $self->owed + $pr_total;
2832 # the customer's current balance as shown on the invoice before this one
2833 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2835 # the change in balance from that invoice to this one
2836 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2838 # the sum of amount owed on all previous invoices
2839 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2841 # the sum of amount owed on all invoices
2842 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2844 # info from customer's last invoice before this one, for some
2846 $invoice_data{'last_bill'} = {};
2847 my $last_bill = $pr_cust_bill[-1];
2849 $invoice_data{'last_bill'} = {
2850 '_date' => $last_bill->_date, #unformatted
2851 # all we need for now
2855 my $summarypage = '';
2856 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2859 $invoice_data{'summarypage'} = $summarypage;
2861 warn "$me substituting variables in notes, footer, smallfooter\n"
2864 my @include = (qw( notes footer smallfooter ));
2865 push @include, 'coupon' unless $params{'no_coupon'};
2866 foreach my $include (@include) {
2868 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2871 if ( $conf->exists($inc_file, $agentnum)
2872 && length( $conf->config($inc_file, $agentnum) ) ) {
2874 @inc_src = $conf->config($inc_file, $agentnum);
2878 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2880 my $convert_map = $convert_maps{$format}{$include};
2882 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2883 s/--\@\]/$delimiters{$format}[1]/g;
2886 &$convert_map( $conf->config($inc_file, $agentnum) );
2890 my $inc_tt = new Text::Template (
2892 SOURCE => [ map "$_\n", @inc_src ],
2893 DELIMITERS => $delimiters{$format},
2894 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2896 unless ( $inc_tt->compile() ) {
2897 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2898 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2902 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2904 $invoice_data{$include} =~ s/\n+$//
2905 if ($format eq 'latex');
2908 # let invoices use either of these as needed
2909 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2910 ? $cust_main->payinfo : '';
2911 $invoice_data{'po_line'} =
2912 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2913 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2916 my %money_chars = ( 'latex' => '',
2917 'html' => $conf->config('money_char') || '$',
2920 my $money_char = $money_chars{$format};
2922 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2923 'html' => $conf->config('money_char') || '$',
2926 my $other_money_char = $other_money_chars{$format};
2927 $invoice_data{'dollar'} = $other_money_char;
2929 my @detail_items = ();
2930 my @total_items = ();
2934 $invoice_data{'detail_items'} = \@detail_items;
2935 $invoice_data{'total_items'} = \@total_items;
2936 $invoice_data{'buf'} = \@buf;
2937 $invoice_data{'sections'} = \@sections;
2939 warn "$me generating sections\n"
2942 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2943 'subtotal' => $other_money_char.
2944 sprintf('%.2f', $pr_total),
2945 'summarized' => '', #why? $summarypage ? 'Y' : '',
2947 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2948 join(' / ', map { $cust_main->balance_date_range(@$_) }
2949 $self->_prior_month30s
2951 if $conf->exists('invoice_include_aging');
2954 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2955 'subtotal' => $taxtotal, # adjusted below
2957 my $tax_weight = _pkg_category($tax_section->{description})
2958 ? _pkg_category($tax_section->{description})->weight
2960 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2961 $tax_section->{'sort_weight'} = $tax_weight;
2964 my $adjusttotal = 0;
2965 my $adjust_section = { 'description' =>
2966 $self->mt('Credits, Payments, and Adjustments'),
2967 'subtotal' => 0, # adjusted below
2969 my $adjust_weight = _pkg_category($adjust_section->{description})
2970 ? _pkg_category($adjust_section->{description})->weight
2972 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2973 $adjust_section->{'sort_weight'} = $adjust_weight;
2975 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2976 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2977 $invoice_data{'multisection'} = $multisection;
2978 my $late_sections = [];
2979 my $extra_sections = [];
2980 my $extra_lines = ();
2981 if ( $multisection ) {
2982 ($extra_sections, $extra_lines) =
2983 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2984 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2986 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2988 push @detail_items, @$extra_lines if $extra_lines;
2990 $self->_items_sections( $late_sections, # this could stand a refactor
2992 $escape_function_nonbsp,
2996 if ($conf->exists('svc_phone_sections')) {
2997 my ($phone_sections, $phone_lines) =
2998 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2999 push @{$late_sections}, @$phone_sections;
3000 push @detail_items, @$phone_lines;
3002 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3003 my ($accountcode_section, $accountcode_lines) =
3004 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3005 if ( scalar(@$accountcode_lines) ) {
3006 push @{$late_sections}, $accountcode_section;
3007 push @detail_items, @$accountcode_lines;
3010 } else {# not multisection
3011 # make a default section
3012 push @sections, { 'description' => '', 'subtotal' => '',
3013 'no_subtotal' => 1 };
3014 # and calculate the finance charge total, since it won't get done otherwise.
3015 # XXX possibly other totals?
3016 # XXX possibly finance_pkgclass should not be used in this manner?
3017 if ( $conf->exists('finance_pkgclass') ) {
3018 my @finance_charges;
3019 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3020 if ( grep { $_->section eq $invoice_data{finance_section} }
3021 $cust_bill_pkg->cust_bill_pkg_display ) {
3022 # I think these are always setup fees, but just to be sure...
3023 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3026 $invoice_data{finance_amount} =
3027 sprintf('%.2f', sum( @finance_charges ) || 0);
3031 unless ( $conf->exists('disable_previous_balance', $agentnum)
3032 || $conf->exists('previous_balance-summary_only')
3036 warn "$me adding previous balances\n"
3039 foreach my $line_item ( $self->_items_previous ) {
3042 ext_description => [],
3044 $detail->{'ref'} = $line_item->{'pkgnum'};
3045 $detail->{'quantity'} = 1;
3046 $detail->{'section'} = $previous_section;
3047 $detail->{'description'} = &$escape_function($line_item->{'description'});
3048 if ( exists $line_item->{'ext_description'} ) {
3049 @{$detail->{'ext_description'}} = map {
3050 &$escape_function($_);
3051 } @{$line_item->{'ext_description'}};
3053 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3054 $line_item->{'amount'};
3055 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3057 push @detail_items, $detail;
3058 push @buf, [ $detail->{'description'},
3059 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3065 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3067 push @buf, ['','-----------'];
3068 push @buf, [ $self->mt('Total Previous Balance'),
3069 $money_char. sprintf("%10.2f", $pr_total) ];
3073 if ( $conf->exists('svc_phone-did-summary') ) {
3074 warn "$me adding DID summary\n"
3077 my ($didsummary,$minutes) = $self->_did_summary;
3078 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3080 { 'description' => $didsummary_desc,
3081 'ext_description' => [ $didsummary, $minutes ],
3085 foreach my $section (@sections, @$late_sections) {
3087 warn "$me adding section \n". Dumper($section)
3090 # begin some normalization
3091 $section->{'subtotal'} = $section->{'amount'}
3093 && !exists($section->{subtotal})
3094 && exists($section->{amount});
3096 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3097 if ( $invoice_data{finance_section} &&
3098 $section->{'description'} eq $invoice_data{finance_section} );
3100 $section->{'subtotal'} = $other_money_char.
3101 sprintf('%.2f', $section->{'subtotal'})
3104 # continue some normalization
3105 $section->{'amount'} = $section->{'subtotal'}
3109 if ( $section->{'description'} ) {
3110 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3115 warn "$me setting options\n"
3118 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3120 $options{'section'} = $section if $multisection;
3121 $options{'format'} = $format;
3122 $options{'escape_function'} = $escape_function;
3123 $options{'no_usage'} = 1 unless $unsquelched;
3124 $options{'unsquelched'} = $unsquelched;
3125 $options{'summary_page'} = $summarypage;
3126 $options{'skip_usage'} =
3127 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3128 $options{'multilocation'} = $multilocation;
3129 $options{'multisection'} = $multisection;
3131 warn "$me searching for line items\n"
3134 foreach my $line_item ( $self->_items_pkg(%options) ) {
3136 warn "$me adding line item $line_item\n"
3140 ext_description => [],
3142 $detail->{'ref'} = $line_item->{'pkgnum'};
3143 $detail->{'quantity'} = $line_item->{'quantity'};
3144 $detail->{'section'} = $section;
3145 $detail->{'description'} = &$escape_function($line_item->{'description'});
3146 if ( exists $line_item->{'ext_description'} ) {
3147 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3149 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3150 $line_item->{'amount'};
3151 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3152 $line_item->{'unit_amount'};
3153 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3155 $detail->{'sdate'} = $line_item->{'sdate'};
3156 $detail->{'edate'} = $line_item->{'edate'};
3157 $detail->{'seconds'} = $line_item->{'seconds'};
3159 push @detail_items, $detail;
3160 push @buf, ( [ $detail->{'description'},
3161 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3163 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3167 if ( $section->{'description'} ) {
3168 push @buf, ( ['','-----------'],
3169 [ $section->{'description'}. ' sub-total',
3170 $section->{'subtotal'} # already formatted this
3179 $invoice_data{current_less_finance} =
3180 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3182 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3183 || $conf->exists('previous_balance-summary_only') )
3185 unshift @sections, $previous_section if $pr_total;
3188 warn "$me adding taxes\n"
3191 foreach my $tax ( $self->_items_tax ) {
3193 $taxtotal += $tax->{'amount'};
3195 my $description = &$escape_function( $tax->{'description'} );
3196 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3198 if ( $multisection ) {
3200 my $money = $old_latex ? '' : $money_char;
3201 push @detail_items, {
3202 ext_description => [],
3205 description => $description,
3206 amount => $money. $amount,
3208 section => $tax_section,
3213 push @total_items, {
3214 'total_item' => $description,
3215 'total_amount' => $other_money_char. $amount,
3220 push @buf,[ $description,
3221 $money_char. $amount,
3228 $total->{'total_item'} = $self->mt('Sub-total');
3229 $total->{'total_amount'} =
3230 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3232 if ( $multisection ) {
3233 $tax_section->{'subtotal'} = $other_money_char.
3234 sprintf('%.2f', $taxtotal);
3235 $tax_section->{'pretotal'} = 'New charges sub-total '.
3236 $total->{'total_amount'};
3237 push @sections, $tax_section if $taxtotal;
3239 unshift @total_items, $total;
3242 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3244 push @buf,['','-----------'];
3245 push @buf,[$self->mt(
3246 $conf->exists('disable_previous_balance', $agentnum)
3248 : 'Total New Charges'
3250 $money_char. sprintf("%10.2f",$self->charged) ];
3256 $item = $conf->config('previous_balance-exclude_from_total')
3257 || 'Total New Charges'
3258 if $conf->exists('previous_balance-exclude_from_total');
3259 my $amount = $self->charged +
3260 ( $conf->exists('disable_previous_balance', $agentnum) ||
3261 $conf->exists('previous_balance-exclude_from_total')
3265 $total->{'total_item'} = &$embolden_function($self->mt($item));
3266 $total->{'total_amount'} =
3267 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3268 if ( $multisection ) {
3269 if ( $adjust_section->{'sort_weight'} ) {
3270 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3271 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3273 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3274 $other_money_char. sprintf('%.2f', $self->charged );
3277 push @total_items, $total;
3279 push @buf,['','-----------'];
3282 sprintf( '%10.2f', $amount )
3287 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3288 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3291 my $credittotal = 0;
3292 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3295 $total->{'total_item'} = &$escape_function($credit->{'description'});
3296 $credittotal += $credit->{'amount'};
3297 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3298 $adjusttotal += $credit->{'amount'};
3299 if ( $multisection ) {
3300 my $money = $old_latex ? '' : $money_char;
3301 push @detail_items, {
3302 ext_description => [],
3305 description => &$escape_function($credit->{'description'}),
3306 amount => $money. $credit->{'amount'},
3308 section => $adjust_section,
3311 push @total_items, $total;
3315 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3318 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3319 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3323 my $paymenttotal = 0;
3324 foreach my $payment ( $self->_items_payments ) {
3326 $total->{'total_item'} = &$escape_function($payment->{'description'});
3327 $paymenttotal += $payment->{'amount'};
3328 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3329 $adjusttotal += $payment->{'amount'};
3330 if ( $multisection ) {
3331 my $money = $old_latex ? '' : $money_char;
3332 push @detail_items, {
3333 ext_description => [],
3336 description => &$escape_function($payment->{'description'}),
3337 amount => $money. $payment->{'amount'},
3339 section => $adjust_section,
3342 push @total_items, $total;
3344 push @buf, [ $payment->{'description'},
3345 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3348 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3350 if ( $multisection ) {
3351 $adjust_section->{'subtotal'} = $other_money_char.
3352 sprintf('%.2f', $adjusttotal);
3353 push @sections, $adjust_section
3354 unless $adjust_section->{sort_weight};
3357 # create Balance Due message
3360 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3361 $total->{'total_amount'} =
3362 &$embolden_function(
3363 $other_money_char. sprintf('%.2f', $summarypage
3365 $self->billing_balance
3366 : $self->owed + $pr_total
3369 if ( $multisection && !$adjust_section->{sort_weight} ) {
3370 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3371 $total->{'total_amount'};
3373 push @total_items, $total;
3375 push @buf,['','-----------'];
3376 push @buf,[$self->balance_due_msg, $money_char.
3377 sprintf("%10.2f", $balance_due ) ];
3380 if ( $conf->exists('previous_balance-show_credit')
3381 and $cust_main->balance < 0 ) {
3382 my $credit_total = {
3383 'total_item' => &$embolden_function($self->credit_balance_msg),
3384 'total_amount' => &$embolden_function(
3385 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3388 if ( $multisection ) {
3389 $adjust_section->{'posttotal'} .= $newline_token .
3390 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3393 push @total_items, $credit_total;
3395 push @buf,['','-----------'];
3396 push @buf,[$self->credit_balance_msg, $money_char.
3397 sprintf("%10.2f", -$cust_main->balance ) ];
3401 if ( $multisection ) {
3402 if ($conf->exists('svc_phone_sections')) {
3404 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3405 $total->{'total_amount'} =
3406 &$embolden_function(
3407 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3409 my $last_section = pop @sections;
3410 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3411 $total->{'total_amount'};
3412 push @sections, $last_section;
3414 push @sections, @$late_sections
3418 # make a discounts-available section, even without multisection
3419 if ( $conf->exists('discount-show_available')
3420 and my @discounts_avail = $self->_items_discounts_avail ) {
3421 my $discount_section = {
3422 'description' => $self->mt('Discounts Available'),
3427 push @sections, $discount_section;
3428 push @detail_items, map { +{
3429 'ref' => '', #should this be something else?
3430 'section' => $discount_section,
3431 'description' => &$escape_function( $_->{description} ),
3432 'amount' => $money_char . &$escape_function( $_->{amount} ),
3433 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3434 } } @discounts_avail;
3437 # All sections and items are built; now fill in templates.
3438 my @includelist = ();
3439 push @includelist, 'summary' if $summarypage;
3440 foreach my $include ( @includelist ) {
3442 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3445 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3447 @inc_src = $conf->config($inc_file, $agentnum);
3451 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3453 my $convert_map = $convert_maps{$format}{$include};
3455 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3456 s/--\@\]/$delimiters{$format}[1]/g;
3459 &$convert_map( $conf->config($inc_file, $agentnum) );
3463 my $inc_tt = new Text::Template (
3465 SOURCE => [ map "$_\n", @inc_src ],
3466 DELIMITERS => $delimiters{$format},
3467 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3469 unless ( $inc_tt->compile() ) {
3470 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3471 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3475 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3477 $invoice_data{$include} =~ s/\n+$//
3478 if ($format eq 'latex');
3483 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3484 /invoice_lines\((\d*)\)/;
3485 $invoice_lines += $1 || scalar(@buf);
3488 die "no invoice_lines() functions in template?"
3489 if ( $format eq 'template' && !$wasfunc );
3491 if ($format eq 'template') {
3493 if ( $invoice_lines ) {
3494 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3495 $invoice_data{'total_pages'}++
3496 if scalar(@buf) % $invoice_lines;
3499 #setup subroutine for the template
3500 $invoice_data{invoice_lines} = sub {
3501 my $lines = shift || scalar(@buf);
3513 push @collect, split("\n",
3514 $text_template->fill_in( HASH => \%invoice_data )
3516 $invoice_data{'page'}++;
3518 map "$_\n", @collect;
3520 # this is where we actually create the invoice
3521 warn "filling in template for invoice ". $self->invnum. "\n"
3523 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3526 $text_template->fill_in(HASH => \%invoice_data);
3530 # helper routine for generating date ranges
3531 sub _prior_month30s {
3534 [ 1, 2592000 ], # 0-30 days ago
3535 [ 2592000, 5184000 ], # 30-60 days ago
3536 [ 5184000, 7776000 ], # 60-90 days ago
3537 [ 7776000, 0 ], # 90+ days ago
3540 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3541 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3546 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3548 Returns an postscript invoice, as a scalar.
3550 Options can be passed as a hashref (recommended) or as a list of time, template
3551 and then any key/value pairs for any other options.
3553 I<time> an optional value used to control the printing of overdue messages. The
3554 default is now. It isn't the date of the invoice; that's the `_date' field.
3555 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3556 L<Time::Local> and L<Date::Parse> for conversion functions.
3558 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3565 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3566 my $ps = generate_ps($file);
3568 unlink($barcodefile) if $barcodefile;
3573 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3575 Returns an PDF invoice, as a scalar.
3577 Options can be passed as a hashref (recommended) or as a list of time, template
3578 and then any key/value pairs for any other options.
3580 I<time> an optional value used to control the printing of overdue messages. The
3581 default is now. It isn't the date of the invoice; that's the `_date' field.
3582 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3583 L<Time::Local> and L<Date::Parse> for conversion functions.
3585 I<template>, if specified, is the name of a suffix for alternate invoices.
3587 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3594 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3595 my $pdf = generate_pdf($file);
3597 unlink($barcodefile) if $barcodefile;
3602 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3604 Returns an HTML invoice, as a scalar.
3606 I<time> an optional value used to control the printing of overdue messages. The
3607 default is now. It isn't the date of the invoice; that's the `_date' field.
3608 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3609 L<Time::Local> and L<Date::Parse> for conversion functions.
3611 I<template>, if specified, is the name of a suffix for alternate invoices.
3613 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3615 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3616 when emailing the invoice as part of a multipart/related MIME email.
3624 %params = %{ shift() };
3626 $params{'time'} = shift;
3627 $params{'template'} = shift;
3628 $params{'cid'} = shift;
3631 $params{'format'} = 'html';
3633 $self->print_generic( %params );
3636 # quick subroutine for print_latex
3638 # There are ten characters that LaTeX treats as special characters, which
3639 # means that they do not simply typeset themselves:
3640 # # $ % & ~ _ ^ \ { }
3642 # TeX ignores blanks following an escaped character; if you want a blank (as
3643 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3647 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3648 $value =~ s/([<>])/\$$1\$/g;
3654 encode_entities($value);
3658 sub _html_escape_nbsp {
3659 my $value = _html_escape(shift);
3660 $value =~ s/ +/ /g;
3664 #utility methods for print_*
3666 sub _translate_old_latex_format {
3667 warn "_translate_old_latex_format called\n"
3674 if ( $line =~ /^%%Detail\s*$/ ) {
3676 push @template, q![@--!,
3677 q! foreach my $_tr_line (@detail_items) {!,
3678 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3679 q! $_tr_line->{'description'} .= !,
3680 q! "\\tabularnewline\n~~".!,
3681 q! join( "\\tabularnewline\n~~",!,
3682 q! @{$_tr_line->{'ext_description'}}!,
3686 while ( ( my $line_item_line = shift )
3687 !~ /^%%EndDetail\s*$/ ) {
3688 $line_item_line =~ s/'/\\'/g; # nice LTS
3689 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3690 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3691 push @template, " \$OUT .= '$line_item_line';";
3694 push @template, '}',
3697 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3699 push @template, '[@--',
3700 ' foreach my $_tr_line (@total_items) {';
3702 while ( ( my $total_item_line = shift )
3703 !~ /^%%EndTotalDetails\s*$/ ) {
3704 $total_item_line =~ s/'/\\'/g; # nice LTS
3705 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3706 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3707 push @template, " \$OUT .= '$total_item_line';";
3710 push @template, '}',
3714 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3715 push @template, $line;
3721 warn "$_\n" foreach @template;
3729 my $conf = $self->conf;
3731 #check for an invoice-specific override
3732 return $self->invoice_terms if $self->invoice_terms;
3734 #check for a customer- specific override
3735 my $cust_main = $self->cust_main;
3736 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3738 #use configured default
3739 $conf->config('invoice_default_terms') || '';
3745 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3746 $duedate = $self->_date() + ( $1 * 86400 );
3753 $self->due_date ? time2str(shift, $self->due_date) : '';
3756 sub balance_due_msg {
3758 my $msg = $self->mt('Balance Due');
3759 return $msg unless $self->terms;
3760 if ( $self->due_date ) {
3761 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3762 $self->due_date2str($date_format);
3763 } elsif ( $self->terms ) {
3764 $msg .= ' - '. $self->terms;
3769 sub balance_due_date {
3771 my $conf = $self->conf;
3773 if ( $conf->exists('invoice_default_terms')
3774 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3775 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3780 sub credit_balance_msg {
3782 $self->mt('Credit Balance Remaining')
3785 =item invnum_date_pretty
3787 Returns a string with the invoice number and date, for example:
3788 "Invoice #54 (3/20/2008)"
3792 sub invnum_date_pretty {
3794 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3799 Returns a string with the date, for example: "3/20/2008"
3805 time2str($date_format, $self->_date);
3808 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3810 Generate section information for all items appearing on this invoice.
3811 This will only be called for multi-section invoices.
3813 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3814 related display records (L<FS::cust_bill_pkg_display>) and organize
3815 them into two groups ("early" and "late" according to whether they come
3816 before or after the total), then into sections. A subtotal is calculated
3819 Section descriptions are returned in sort weight order. Each consists
3820 of a hash containing:
3822 description: the package category name, escaped
3823 subtotal: the total charges in that section
3824 tax_section: a flag indicating that the section contains only tax charges
3825 summarized: same as tax_section, for some reason
3826 sort_weight: the package category's sort weight
3828 If 'condense' is set on the display record, it also contains everything
3829 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3830 coderefs to generate parts of the invoice. This is not advised.
3834 LATE: an arrayref to push the "late" section hashes onto. The "early"
3835 group is simply returned from the method.
3837 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3838 Turning this on has the following effects:
3839 - Ignores display items with the 'summary' flag.
3840 - Combines all items into the "early" group.
3841 - Creates sections for all non-disabled package categories, even if they
3842 have no charges on this invoice, as well as a section with no name.
3844 ESCAPE: an escape function to use for section titles.
3846 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3847 sorted list. If there are any of these, section subtotals exclude
3850 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3851 passed through to C<_condense_section()>.
3855 use vars qw(%pkg_category_cache);
3856 sub _items_sections {
3859 my $summarypage = shift;
3861 my $extra_sections = shift;
3865 my %late_subtotal = ();
3868 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3871 my $usage = $cust_bill_pkg->usage;
3873 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3874 next if ( $display->summary && $summarypage );
3876 my $section = $display->section;
3877 my $type = $display->type;
3879 $not_tax{$section} = 1
3880 unless $cust_bill_pkg->pkgnum == 0;
3882 if ( $display->post_total && !$summarypage ) {
3883 if (! $type || $type eq 'S') {
3884 $late_subtotal{$section} += $cust_bill_pkg->setup
3885 if $cust_bill_pkg->setup != 0;
3889 $late_subtotal{$section} += $cust_bill_pkg->recur
3890 if $cust_bill_pkg->recur != 0;
3893 if ($type && $type eq 'R') {
3894 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3895 if $cust_bill_pkg->recur != 0;
3898 if ($type && $type eq 'U') {
3899 $late_subtotal{$section} += $usage
3900 unless scalar(@$extra_sections);
3905 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3907 if (! $type || $type eq 'S') {
3908 $subtotal{$section} += $cust_bill_pkg->setup
3909 if $cust_bill_pkg->setup != 0;
3913 $subtotal{$section} += $cust_bill_pkg->recur
3914 if $cust_bill_pkg->recur != 0;
3917 if ($type && $type eq 'R') {
3918 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3919 if $cust_bill_pkg->recur != 0;
3922 if ($type && $type eq 'U') {
3923 $subtotal{$section} += $usage
3924 unless scalar(@$extra_sections);
3933 %pkg_category_cache = ();
3935 push @$late, map { { 'description' => &{$escape}($_),
3936 'subtotal' => $late_subtotal{$_},
3938 'sort_weight' => ( _pkg_category($_)
3939 ? _pkg_category($_)->weight
3942 ((_pkg_category($_) && _pkg_category($_)->condense)
3943 ? $self->_condense_section($format)
3947 sort _sectionsort keys %late_subtotal;
3950 if ( $summarypage ) {
3951 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3952 map { $_->categoryname } qsearch('pkg_category', {});
3953 push @sections, '' if exists($subtotal{''});
3955 @sections = keys %subtotal;
3958 my @early = map { { 'description' => &{$escape}($_),
3959 'subtotal' => $subtotal{$_},
3960 'summarized' => $not_tax{$_} ? '' : 'Y',
3961 'tax_section' => $not_tax{$_} ? '' : 'Y',
3962 'sort_weight' => ( _pkg_category($_)
3963 ? _pkg_category($_)->weight
3966 ((_pkg_category($_) && _pkg_category($_)->condense)
3967 ? $self->_condense_section($format)
3972 push @early, @$extra_sections if $extra_sections;
3974 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3978 #helper subs for above
3981 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3985 my $categoryname = shift;
3986 $pkg_category_cache{$categoryname} ||=
3987 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3990 my %condensed_format = (
3991 'label' => [ qw( Description Qty Amount ) ],
3993 sub { shift->{description} },
3994 sub { shift->{quantity} },
3995 sub { my($href, %opt) = @_;
3996 ($opt{dollar} || ''). $href->{amount};
3999 'align' => [ qw( l r r ) ],
4000 'span' => [ qw( 5 1 1 ) ], # unitprices?
4001 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4004 sub _condense_section {
4005 my ( $self, $format ) = ( shift, shift );
4007 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4008 qw( description_generator
4011 total_line_generator
4016 sub _condensed_generator_defaults {
4017 my ( $self, $format ) = ( shift, shift );
4018 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4027 sub _condensed_header_generator {
4028 my ( $self, $format ) = ( shift, shift );
4030 my ( $f, $prefix, $suffix, $separator, $column ) =
4031 _condensed_generator_defaults($format);
4033 if ($format eq 'latex') {
4034 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4035 $suffix = "\\\\\n\\hline";
4038 sub { my ($d,$a,$s,$w) = @_;
4039 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4041 } elsif ( $format eq 'html' ) {
4042 $prefix = '<th></th>';
4046 sub { my ($d,$a,$s,$w) = @_;
4047 return qq!<th align="$html_align{$a}">$d</th>!;
4055 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4057 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4060 $prefix. join($separator, @result). $suffix;
4065 sub _condensed_description_generator {
4066 my ( $self, $format ) = ( shift, shift );
4068 my ( $f, $prefix, $suffix, $separator, $column ) =
4069 _condensed_generator_defaults($format);
4071 my $money_char = '$';
4072 if ($format eq 'latex') {
4073 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4075 $separator = " & \n";
4077 sub { my ($d,$a,$s,$w) = @_;
4078 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4080 $money_char = '\\dollar';
4081 }elsif ( $format eq 'html' ) {
4082 $prefix = '"><td align="center"></td>';
4086 sub { my ($d,$a,$s,$w) = @_;
4087 return qq!<td align="$html_align{$a}">$d</td>!;
4089 #$money_char = $conf->config('money_char') || '$';
4090 $money_char = ''; # this is madness
4098 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4100 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4102 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4103 map { $f->{$_}->[$i] } qw(align span width)
4107 $prefix. join( $separator, @result ). $suffix;
4112 sub _condensed_total_generator {
4113 my ( $self, $format ) = ( shift, shift );
4115 my ( $f, $prefix, $suffix, $separator, $column ) =
4116 _condensed_generator_defaults($format);
4119 if ($format eq 'latex') {
4122 $separator = " & \n";
4124 sub { my ($d,$a,$s,$w) = @_;
4125 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4127 }elsif ( $format eq 'html' ) {
4131 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4133 sub { my ($d,$a,$s,$w) = @_;
4134 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4143 # my $r = &{$f->{fields}->[$i]}(@args);
4144 # $r .= ' Total' unless $i;
4146 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4148 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4149 map { $f->{$_}->[$i] } qw(align span width)
4153 $prefix. join( $separator, @result ). $suffix;
4158 =item total_line_generator FORMAT
4160 Returns a coderef used for generation of invoice total line items for this
4161 usage_class. FORMAT is either html or latex
4165 # should not be used: will have issues with hash element names (description vs
4166 # total_item and amount vs total_amount -- another array of functions?
4168 sub _condensed_total_line_generator {
4169 my ( $self, $format ) = ( shift, shift );
4171 my ( $f, $prefix, $suffix, $separator, $column ) =
4172 _condensed_generator_defaults($format);
4175 if ($format eq 'latex') {
4178 $separator = " & \n";
4180 sub { my ($d,$a,$s,$w) = @_;
4181 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4183 }elsif ( $format eq 'html' ) {
4187 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4189 sub { my ($d,$a,$s,$w) = @_;
4190 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4199 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4201 &{$column}( &{$f->{fields}->[$i]}(@args),
4202 map { $f->{$_}->[$i] } qw(align span width)
4206 $prefix. join( $separator, @result ). $suffix;
4211 #sub _items_extra_usage_sections {
4213 # my $escape = shift;
4215 # my %sections = ();
4217 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4218 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4220 # next unless $cust_bill_pkg->pkgnum > 0;
4222 # foreach my $section ( keys %usage_class ) {
4224 # my $usage = $cust_bill_pkg->usage($section);
4226 # next unless $usage && $usage > 0;
4228 # $sections{$section} ||= 0;
4229 # $sections{$section} += $usage;
4235 # map { { 'description' => &{$escape}($_),
4236 # 'subtotal' => $sections{$_},
4237 # 'summarized' => '',
4238 # 'tax_section' => '',
4241 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4245 sub _items_extra_usage_sections {
4247 my $conf = $self->conf;
4255 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4257 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4258 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4259 next unless $cust_bill_pkg->pkgnum > 0;
4261 foreach my $classnum ( keys %usage_class ) {
4262 my $section = $usage_class{$classnum}->classname;
4263 $classnums{$section} = $classnum;
4265 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4266 my $amount = $detail->amount;
4267 next unless $amount && $amount > 0;
4269 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4270 $sections{$section}{amount} += $amount; #subtotal
4271 $sections{$section}{calls}++;
4272 $sections{$section}{duration} += $detail->duration;
4274 my $desc = $detail->regionname;
4275 my $description = $desc;
4276 $description = substr($desc, 0, $maxlength). '...'
4277 if $format eq 'latex' && length($desc) > $maxlength;
4279 $lines{$section}{$desc} ||= {
4280 description => &{$escape}($description),
4281 #pkgpart => $part_pkg->pkgpart,
4282 pkgnum => $cust_bill_pkg->pkgnum,
4287 #unit_amount => $cust_bill_pkg->unitrecur,
4288 quantity => $cust_bill_pkg->quantity,
4289 product_code => 'N/A',
4290 ext_description => [],
4293 $lines{$section}{$desc}{amount} += $amount;
4294 $lines{$section}{$desc}{calls}++;
4295 $lines{$section}{$desc}{duration} += $detail->duration;
4301 my %sectionmap = ();
4302 foreach (keys %sections) {
4303 my $usage_class = $usage_class{$classnums{$_}};
4304 $sectionmap{$_} = { 'description' => &{$escape}($_),
4305 'amount' => $sections{$_}{amount}, #subtotal
4306 'calls' => $sections{$_}{calls},
4307 'duration' => $sections{$_}{duration},
4309 'tax_section' => '',
4310 'sort_weight' => $usage_class->weight,
4311 ( $usage_class->format
4312 ? ( map { $_ => $usage_class->$_($format) }
4313 qw( description_generator header_generator total_generator total_line_generator )
4320 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4324 foreach my $section ( keys %lines ) {
4325 foreach my $line ( keys %{$lines{$section}} ) {
4326 my $l = $lines{$section}{$line};
4327 $l->{section} = $sectionmap{$section};
4328 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4329 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4334 return(\@sections, \@lines);
4340 my $end = $self->_date;
4342 # start at date of previous invoice + 1 second or 0 if no previous invoice
4343 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4344 $start = 0 if !$start;
4347 my $cust_main = $self->cust_main;
4348 my @pkgs = $cust_main->all_pkgs;
4349 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4352 foreach my $pkg ( @pkgs ) {
4353 my @h_cust_svc = $pkg->h_cust_svc($end);
4354 foreach my $h_cust_svc ( @h_cust_svc ) {
4355 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4356 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4358 my $inserted = $h_cust_svc->date_inserted;
4359 my $deleted = $h_cust_svc->date_deleted;
4360 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4362 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4364 # DID either activated or ported in; cannot be both for same DID simultaneously
4365 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4366 && (!$phone_inserted->lnp_status
4367 || $phone_inserted->lnp_status eq ''
4368 || $phone_inserted->lnp_status eq 'native')) {
4371 else { # this one not so clean, should probably move to (h_)svc_phone
4372 my $phone_portedin = qsearchs( 'h_svc_phone',
4373 { 'svcnum' => $h_cust_svc->svcnum,
4374 'lnp_status' => 'portedin' },
4375 FS::h_svc_phone->sql_h_searchs($end),
4377 $num_portedin++ if $phone_portedin;
4380 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4381 if($deleted >= $start && $deleted <= $end && $phone_deleted
4382 && (!$phone_deleted->lnp_status
4383 || $phone_deleted->lnp_status ne 'portingout')) {
4386 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4387 && $phone_deleted->lnp_status
4388 && $phone_deleted->lnp_status eq 'portingout') {
4392 # increment usage minutes
4393 if ( $phone_inserted ) {
4394 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4395 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4398 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4401 # don't look at this service again
4402 push @seen, $h_cust_svc->svcnum;
4406 $minutes = sprintf("%d", $minutes);
4407 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4408 . "$num_deactivated Ported-Out: $num_portedout ",
4409 "Total Minutes: $minutes");
4412 sub _items_accountcode_cdr {
4417 my $section = { 'amount' => 0,
4420 'sort_weight' => '',
4422 'description' => 'Usage by Account Code',
4428 my %accountcodes = ();
4430 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4431 next unless $cust_bill_pkg->pkgnum > 0;
4433 my @header = $cust_bill_pkg->details_header;
4434 next unless scalar(@header);
4435 $section->{'header'} = join(',',@header);
4437 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4439 $section->{'header'} = $detail->formatted('format' => $format)
4440 if($detail->detail eq $section->{'header'});
4442 my $accountcode = $detail->accountcode;
4443 next unless $accountcode;
4445 my $amount = $detail->amount;
4446 next unless $amount && $amount > 0;
4448 $accountcodes{$accountcode} ||= {
4449 description => $accountcode,
4456 product_code => 'N/A',
4457 section => $section,
4458 ext_description => [ $section->{'header'} ],
4462 $section->{'amount'} += $amount;
4463 $accountcodes{$accountcode}{'amount'} += $amount;
4464 $accountcodes{$accountcode}{calls}++;
4465 $accountcodes{$accountcode}{duration} += $detail->duration;
4466 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4470 foreach my $l ( values %accountcodes ) {
4471 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4472 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4473 foreach my $sorted_detail ( @sorted_detail ) {
4474 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4476 delete $l->{detail_temp};
4480 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4482 return ($section,\@sorted_lines);
4485 sub _items_svc_phone_sections {
4487 my $conf = $self->conf;
4495 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4497 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4498 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4500 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4501 next unless $cust_bill_pkg->pkgnum > 0;
4503 my @header = $cust_bill_pkg->details_header;
4504 next unless scalar(@header);
4506 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4508 my $phonenum = $detail->phonenum;
4509 next unless $phonenum;
4511 my $amount = $detail->amount;
4512 next unless $amount && $amount > 0;
4514 $sections{$phonenum} ||= { 'amount' => 0,
4517 'sort_weight' => -1,
4518 'phonenum' => $phonenum,
4520 $sections{$phonenum}{amount} += $amount; #subtotal
4521 $sections{$phonenum}{calls}++;
4522 $sections{$phonenum}{duration} += $detail->duration;
4524 my $desc = $detail->regionname;
4525 my $description = $desc;
4526 $description = substr($desc, 0, $maxlength). '...'
4527 if $format eq 'latex' && length($desc) > $maxlength;
4529 $lines{$phonenum}{$desc} ||= {
4530 description => &{$escape}($description),
4531 #pkgpart => $part_pkg->pkgpart,
4539 product_code => 'N/A',
4540 ext_description => [],
4543 $lines{$phonenum}{$desc}{amount} += $amount;
4544 $lines{$phonenum}{$desc}{calls}++;
4545 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4547 my $line = $usage_class{$detail->classnum}->classname;
4548 $sections{"$phonenum $line"} ||=
4552 'sort_weight' => $usage_class{$detail->classnum}->weight,
4553 'phonenum' => $phonenum,
4554 'header' => [ @header ],
4556 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4557 $sections{"$phonenum $line"}{calls}++;
4558 $sections{"$phonenum $line"}{duration} += $detail->duration;
4560 $lines{"$phonenum $line"}{$desc} ||= {
4561 description => &{$escape}($description),
4562 #pkgpart => $part_pkg->pkgpart,
4570 product_code => 'N/A',
4571 ext_description => [],
4574 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4575 $lines{"$phonenum $line"}{$desc}{calls}++;
4576 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4577 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4578 $detail->formatted('format' => $format);
4583 my %sectionmap = ();
4584 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4585 foreach ( keys %sections ) {
4586 my @header = @{ $sections{$_}{header} || [] };
4588 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4589 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4590 my $usage_class = $summary ? $simple : $usage_simple;
4591 my $ending = $summary ? ' usage charges' : '';
4594 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4596 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4597 'amount' => $sections{$_}{amount}, #subtotal
4598 'calls' => $sections{$_}{calls},
4599 'duration' => $sections{$_}{duration},
4601 'tax_section' => '',
4602 'phonenum' => $sections{$_}{phonenum},
4603 'sort_weight' => $sections{$_}{sort_weight},
4604 'post_total' => $summary, #inspire pagebreak
4606 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4607 qw( description_generator
4610 total_line_generator
4617 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4618 $a->{sort_weight} <=> $b->{sort_weight}
4623 foreach my $section ( keys %lines ) {
4624 foreach my $line ( keys %{$lines{$section}} ) {
4625 my $l = $lines{$section}{$line};
4626 $l->{section} = $sectionmap{$section};
4627 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4628 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4633 if($conf->exists('phone_usage_class_summary')) {
4634 # this only works with Latex
4638 # after this, we'll have only two sections per DID:
4639 # Calls Summary and Calls Detail
4640 foreach my $section ( @sections ) {
4641 if($section->{'post_total'}) {
4642 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4643 $section->{'total_line_generator'} = sub { '' };
4644 $section->{'total_generator'} = sub { '' };
4645 $section->{'header_generator'} = sub { '' };
4646 $section->{'description_generator'} = '';
4647 push @newsections, $section;
4648 my %calls_detail = %$section;
4649 $calls_detail{'post_total'} = '';
4650 $calls_detail{'sort_weight'} = '';
4651 $calls_detail{'description_generator'} = sub { '' };
4652 $calls_detail{'header_generator'} = sub {
4653 return ' & Date/Time & Called Number & Duration & Price'
4654 if $format eq 'latex';
4657 $calls_detail{'description'} = 'Calls Detail: '
4658 . $section->{'phonenum'};
4659 push @newsections, \%calls_detail;
4663 # after this, each usage class is collapsed/summarized into a single
4664 # line under the Calls Summary section
4665 foreach my $newsection ( @newsections ) {
4666 if($newsection->{'post_total'}) { # this means Calls Summary
4667 foreach my $section ( @sections ) {
4668 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4669 && !$section->{'post_total'});
4670 my $newdesc = $section->{'description'};
4671 my $tn = $section->{'phonenum'};
4672 $newdesc =~ s/$tn//g;
4673 my $line = { ext_description => [],
4677 calls => $section->{'calls'},
4678 section => $newsection,
4679 duration => $section->{'duration'},
4680 description => $newdesc,
4681 amount => sprintf("%.2f",$section->{'amount'}),
4682 product_code => 'N/A',
4684 push @newlines, $line;
4689 # after this, Calls Details is populated with all CDRs
4690 foreach my $newsection ( @newsections ) {
4691 if(!$newsection->{'post_total'}) { # this means Calls Details
4692 foreach my $line ( @lines ) {
4693 next unless (scalar(@{$line->{'ext_description'}}) &&
4694 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4696 my @extdesc = @{$line->{'ext_description'}};
4698 foreach my $extdesc ( @extdesc ) {
4699 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4700 push @newextdesc, $extdesc;
4702 $line->{'ext_description'} = \@newextdesc;
4703 $line->{'section'} = $newsection;
4704 push @newlines, $line;
4709 return(\@newsections, \@newlines);
4712 return(\@sections, \@lines);
4716 sub _items { # seems to be unused
4719 #my @display = scalar(@_)
4721 # : qw( _items_previous _items_pkg );
4722 # #: qw( _items_pkg );
4723 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4724 my @display = qw( _items_previous _items_pkg );
4727 foreach my $display ( @display ) {
4728 push @b, $self->$display(@_);
4733 sub _items_previous {
4735 my $conf = $self->conf;
4736 my $cust_main = $self->cust_main;
4737 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4739 foreach ( @pr_cust_bill ) {
4740 my $date = $conf->exists('invoice_show_prior_due_date')
4741 ? 'due '. $_->due_date2str($date_format)
4742 : time2str($date_format, $_->_date);
4744 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4745 #'pkgpart' => 'N/A',
4747 'amount' => sprintf("%.2f", $_->owed),
4753 # 'description' => 'Previous Balance',
4754 # #'pkgpart' => 'N/A',
4755 # 'pkgnum' => 'N/A',
4756 # 'amount' => sprintf("%10.2f", $pr_total ),
4757 # 'ext_description' => [ map {
4758 # "Invoice ". $_->invnum.
4759 # " (". time2str("%x",$_->_date). ") ".
4760 # sprintf("%10.2f", $_->owed)
4761 # } @pr_cust_bill ],
4766 =item _items_pkg [ OPTIONS ]
4768 Return line item hashes for each package item on this invoice. Nearly
4771 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4773 The only OPTIONS accepted is 'section', which may point to a hashref
4774 with a key named 'condensed', which may have a true value. If it
4775 does, this method tries to merge identical items into items with
4776 'quantity' equal to the number of items (not the sum of their
4777 separate quantities, for some reason).
4785 warn "$me _items_pkg searching for all package line items\n"
4788 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4790 warn "$me _items_pkg filtering line items\n"
4792 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4794 if ($options{section} && $options{section}->{condensed}) {
4796 warn "$me _items_pkg condensing section\n"
4800 local $Storable::canonical = 1;
4801 foreach ( @items ) {
4803 delete $item->{ref};
4804 delete $item->{ext_description};
4805 my $key = freeze($item);
4806 $itemshash{$key} ||= 0;
4807 $itemshash{$key} ++; # += $item->{quantity};
4809 @items = sort { $a->{description} cmp $b->{description} }
4810 map { my $i = thaw($_);
4811 $i->{quantity} = $itemshash{$_};
4813 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4819 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4826 return 0 unless $a->itemdesc cmp $b->itemdesc;
4827 return -1 if $b->itemdesc eq 'Tax';
4828 return 1 if $a->itemdesc eq 'Tax';
4829 return -1 if $b->itemdesc eq 'Other surcharges';
4830 return 1 if $a->itemdesc eq 'Other surcharges';
4831 $a->itemdesc cmp $b->itemdesc;
4836 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4837 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4840 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4842 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4843 list of hashrefs describing the line items they generate on the invoice.
4845 OPTIONS may include:
4847 format: the invoice format.
4849 escape_function: the function used to escape strings.
4851 DEPRECATED? (expensive, mostly unused?)
4852 format_function: the function used to format CDRs.
4854 section: a hashref containing 'description'; if this is present,
4855 cust_bill_pkg_display records not belonging to this section are
4858 multisection: a flag indicating that this is a multisection invoice,
4859 which does something complicated.
4861 multilocation: a flag to display the location label for the package.
4863 Returns a list of hashrefs, each of which may contain:
4865 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4866 ext_description, which is an arrayref of detail lines to show below
4871 sub _items_cust_bill_pkg {
4873 my $conf = $self->conf;
4874 my $cust_bill_pkgs = shift;
4877 my $format = $opt{format} || '';
4878 my $escape_function = $opt{escape_function} || sub { shift };
4879 my $format_function = $opt{format_function} || '';
4880 my $no_usage = $opt{no_usage} || '';
4881 my $unsquelched = $opt{unsquelched} || ''; #unused
4882 my $section = $opt{section}->{description} if $opt{section};
4883 my $summary_page = $opt{summary_page} || ''; #unused
4884 my $multilocation = $opt{multilocation} || '';
4885 my $multisection = $opt{multisection} || '';
4886 my $discount_show_always = 0;
4888 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4890 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4893 my ($s, $r, $u) = ( undef, undef, undef );
4894 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4897 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4898 if ( $_ && !$cust_bill_pkg->hidden ) {
4899 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4900 $_->{amount} =~ s/^\-0\.00$/0.00/;
4901 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4903 if $_->{amount} != 0
4904 || $discount_show_always
4905 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4906 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4912 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4913 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4916 foreach my $display ( grep { defined($section)
4917 ? $_->section eq $section
4920 #grep { !$_->summary || !$summary_page } # bunk!
4921 grep { !$_->summary || $multisection }
4922 $cust_bill_pkg->cust_bill_pkg_display
4926 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4927 $display->billpkgdisplaynum. "\n"
4930 my $type = $display->type;
4932 my $desc = $cust_bill_pkg->desc;
4933 $desc = substr($desc, 0, $maxlength). '...'
4934 if $format eq 'latex' && length($desc) > $maxlength;
4936 my %details_opt = ( 'format' => $format,
4937 'escape_function' => $escape_function,
4938 'format_function' => $format_function,
4939 'no_usage' => $opt{'no_usage'},
4942 if ( $cust_bill_pkg->pkgnum > 0 ) {
4944 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4947 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4949 # start/end dates for invoice formats that do nonstandard
4951 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4953 if ( (!$type || $type eq 'S')
4954 && ( $cust_bill_pkg->setup != 0
4955 || $cust_bill_pkg->setup_show_zero
4960 warn "$me _items_cust_bill_pkg adding setup\n"
4963 my $description = $desc;
4964 $description .= ' Setup'
4965 if $cust_bill_pkg->recur != 0
4966 || $discount_show_always
4967 || $cust_bill_pkg->recur_show_zero;
4970 unless ( $cust_pkg->part_pkg->hide_svc_detail
4971 || $cust_bill_pkg->hidden )
4974 push @d, map &{$escape_function}($_),
4975 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4976 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4978 if ( $multilocation ) {
4979 my $loc = $cust_pkg->location_label;
4980 $loc = substr($loc, 0, $maxlength). '...'
4981 if $format eq 'latex' && length($loc) > $maxlength;
4982 push @d, &{$escape_function}($loc);
4985 } #unless hiding service details
4987 push @d, $cust_bill_pkg->details(%details_opt)
4988 if $cust_bill_pkg->recur == 0;
4990 if ( $cust_bill_pkg->hidden ) {
4991 $s->{amount} += $cust_bill_pkg->setup;
4992 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4993 push @{ $s->{ext_description} }, @d;
4997 description => $description,
4998 #pkgpart => $part_pkg->pkgpart,
4999 pkgnum => $cust_bill_pkg->pkgnum,
5000 amount => $cust_bill_pkg->setup,
5001 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5002 unit_amount => $cust_bill_pkg->unitsetup,
5003 quantity => $cust_bill_pkg->quantity,
5004 ext_description => \@d,
5010 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5012 $cust_bill_pkg->recur != 0
5013 || $cust_bill_pkg->setup == 0
5014 || $discount_show_always
5015 || $cust_bill_pkg->recur_show_zero
5020 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5023 my $is_summary = $display->summary;
5024 my $description = ($is_summary && $type && $type eq 'U')
5025 ? "Usage charges" : $desc;
5027 #pry be a bit more efficient to look some of this conf stuff up
5030 $conf->exists('disable_line_item_date_ranges')
5031 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5034 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5035 $cust_main->agentnum
5037 if ( defined($date_style) && $date_style eq 'month_of' ) {
5038 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5039 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5040 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5041 $cust_main->agentnum
5043 $desc .= ' ' unless $desc =~ /\s$/;
5044 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5046 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5047 " - ". time2str($date_format, $cust_bill_pkg->edate);
5049 $description .= " ($time_period)";
5053 my @seconds = (); # for display of usage info
5055 #at least until cust_bill_pkg has "past" ranges in addition to
5056 #the "future" sdate/edate ones... see #3032
5057 my @dates = ( $self->_date );
5058 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5059 push @dates, $prev->sdate if $prev;
5060 push @dates, undef if !$prev;
5062 unless ( $cust_pkg->part_pkg->hide_svc_detail
5063 || $cust_bill_pkg->itemdesc
5064 || $cust_bill_pkg->hidden
5065 || $is_summary && $type && $type eq 'U' )
5068 warn "$me _items_cust_bill_pkg adding service details\n"
5071 push @d, map &{$escape_function}($_),
5072 $cust_pkg->h_labels_short(@dates, 'I')
5073 #$cust_bill_pkg->edate,
5074 #$cust_bill_pkg->sdate)
5075 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5077 warn "$me _items_cust_bill_pkg done adding service details\n"
5080 if ( $multilocation ) {
5081 my $loc = $cust_pkg->location_label;
5082 $loc = substr($loc, 0, $maxlength). '...'
5083 if $format eq 'latex' && length($loc) > $maxlength;
5084 push @d, &{$escape_function}($loc);
5087 # Display of seconds_since_sqlradacct:
5088 # On the invoice, when processing @detail_items, look for a field
5089 # named 'seconds'. This will contain total seconds for each
5090 # service, in the same order as @ext_description. For services
5091 # that don't support this it will show undef.
5092 if ( $conf->exists('svc_acct-usage_seconds')
5093 and ! $cust_bill_pkg->pkgpart_override ) {
5094 foreach my $cust_svc (
5095 $cust_pkg->h_cust_svc(@dates, 'I')
5098 # eval because not having any part_export_usage exports
5099 # is a fatal error, last_bill/_date because that's how
5100 # sqlradius_hour billing does it
5102 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5104 push @seconds, $sec;
5106 } #if svc_acct-usage_seconds
5110 unless ( $is_summary ) {
5111 warn "$me _items_cust_bill_pkg adding details\n"
5114 #instead of omitting details entirely in this case (unwanted side
5115 # effects), just omit CDRs
5116 $details_opt{'no_usage'} = 1
5117 if $type && $type eq 'R';
5119 push @d, $cust_bill_pkg->details(%details_opt);
5122 warn "$me _items_cust_bill_pkg calculating amount\n"
5127 $amount = $cust_bill_pkg->recur;
5128 } elsif ($type eq 'R') {
5129 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5130 } elsif ($type eq 'U') {
5131 $amount = $cust_bill_pkg->usage;
5134 if ( !$type || $type eq 'R' ) {
5136 warn "$me _items_cust_bill_pkg adding recur\n"
5139 if ( $cust_bill_pkg->hidden ) {
5140 $r->{amount} += $amount;
5141 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5142 push @{ $r->{ext_description} }, @d;
5145 description => $description,
5146 #pkgpart => $part_pkg->pkgpart,
5147 pkgnum => $cust_bill_pkg->pkgnum,
5149 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5150 unit_amount => $cust_bill_pkg->unitrecur,
5151 quantity => $cust_bill_pkg->quantity,
5153 ext_description => \@d,
5155 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5158 } else { # $type eq 'U'
5160 warn "$me _items_cust_bill_pkg adding usage\n"
5163 if ( $cust_bill_pkg->hidden ) {
5164 $u->{amount} += $amount;
5165 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5166 push @{ $u->{ext_description} }, @d;
5169 description => $description,
5170 #pkgpart => $part_pkg->pkgpart,
5171 pkgnum => $cust_bill_pkg->pkgnum,
5173 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5174 unit_amount => $cust_bill_pkg->unitrecur,
5175 quantity => $cust_bill_pkg->quantity,
5177 ext_description => \@d,
5182 } # recurring or usage with recurring charge
5184 } else { #pkgnum tax or one-shot line item (??)
5186 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5189 if ( $cust_bill_pkg->setup != 0 ) {
5191 'description' => $desc,
5192 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5195 if ( $cust_bill_pkg->recur != 0 ) {
5197 'description' => "$desc (".
5198 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5199 time2str($date_format, $cust_bill_pkg->edate). ')',
5200 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5208 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5209 && $conf->exists('discount-show-always'));
5213 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5215 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5216 $_->{amount} =~ s/^\-0\.00$/0.00/;
5217 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5219 if $_->{amount} != 0
5220 || $discount_show_always
5221 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5222 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5226 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5233 sub _items_credits {
5234 my( $self, %opt ) = @_;
5235 my $trim_len = $opt{'trim_len'} || 60;
5239 foreach ( $self->cust_credited ) {
5241 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5243 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5244 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5245 $reason = " ($reason) " if $reason;
5248 #'description' => 'Credit ref\#'. $_->crednum.
5249 # " (". time2str("%x",$_->cust_credit->_date) .")".
5251 'description' => $self->mt('Credit applied').' '.
5252 time2str($date_format,$_->cust_credit->_date). $reason,
5253 'amount' => sprintf("%.2f",$_->amount),
5261 sub _items_payments {
5265 #get & print payments
5266 foreach ( $self->cust_bill_pay ) {
5268 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5271 'description' => $self->mt('Payment received').' '.
5272 time2str($date_format,$_->cust_pay->_date ),
5273 'amount' => sprintf("%.2f", $_->amount )
5281 =item _items_discounts_avail
5283 Returns an array of line item hashrefs representing available term discounts
5284 for this invoice. This makes the same assumptions that apply to term
5285 discounts in general: that the package is billed monthly, at a flat rate,
5286 with no usage charges. A prorated first month will be handled, as will
5287 a setup fee if the discount is allowed to apply to setup fees.
5291 sub _items_discounts_avail {
5293 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5295 my %plans = $self->discount_plans;
5297 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5301 my $plan = $plans{$months};
5303 my $term_total = sprintf('%.2f', $plan->discounted_total);
5304 my $percent = sprintf('%.0f',
5305 100 * (1 - $term_total / $plan->base_total) );
5306 my $permonth = sprintf('%.2f', $term_total / $months);
5307 my $detail = $self->mt('discount on item'). ' '.
5308 join(', ', map { "#$_" } $plan->pkgnums)
5311 # discounts for non-integer months don't work anyway
5312 $months = sprintf("%d", $months);
5315 description => $self->mt('Save [_1]% by paying for [_2] months',
5317 amount => $self->mt('[_1] ([_2] per month)',
5318 $term_total, $money_char.$permonth),
5319 ext_description => ($detail || ''),
5322 sort { $b <=> $a } keys %plans;
5326 =item call_details [ OPTION => VALUE ... ]
5328 Returns an array of CSV strings representing the call details for this invoice
5329 The only option available is the boolean prepend_billed_number
5334 my ($self, %opt) = @_;
5336 my $format_function = sub { shift };
5338 if ($opt{prepend_billed_number}) {
5339 $format_function = sub {
5343 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5348 my @details = map { $_->details( 'format_function' => $format_function,
5349 'escape_function' => sub{ return() },
5353 $self->cust_bill_pkg;
5354 my $header = $details[0];
5355 ( $header, grep { $_ ne $header } @details );
5365 =item process_reprint
5369 sub process_reprint {
5370 process_re_X('print', @_);
5373 =item process_reemail
5377 sub process_reemail {
5378 process_re_X('email', @_);
5386 process_re_X('fax', @_);
5394 process_re_X('ftp', @_);
5401 sub process_respool {
5402 process_re_X('spool', @_);
5405 use Storable qw(thaw);
5409 my( $method, $job ) = ( shift, shift );
5410 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5412 my $param = thaw(decode_base64(shift));
5413 warn Dumper($param) if $DEBUG;
5424 my($method, $job, %param ) = @_;
5426 warn "re_X $method for job $job with param:\n".
5427 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5430 #some false laziness w/search/cust_bill.html
5432 my $orderby = 'ORDER BY cust_bill._date';
5434 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5436 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5438 my @cust_bill = qsearch( {
5439 #'select' => "cust_bill.*",
5440 'table' => 'cust_bill',
5441 'addl_from' => $addl_from,
5443 'extra_sql' => $extra_sql,
5444 'order_by' => $orderby,
5448 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5450 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5453 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5454 foreach my $cust_bill ( @cust_bill ) {
5455 $cust_bill->$method();
5457 if ( $job ) { #progressbar foo
5459 if ( time - $min_sec > $last ) {
5460 my $error = $job->update_statustext(
5461 int( 100 * $num / scalar(@cust_bill) )
5463 die $error if $error;
5474 =head1 CLASS METHODS
5480 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5485 my ($class, $start, $end) = @_;
5487 $class->paid_sql($start, $end). ' - '.
5488 $class->credited_sql($start, $end);
5493 Returns an SQL fragment to retreive the net amount (charged minus credited).
5498 my ($class, $start, $end) = @_;
5499 'charged - '. $class->credited_sql($start, $end);
5504 Returns an SQL fragment to retreive the amount paid against this invoice.
5509 my ($class, $start, $end) = @_;
5510 $start &&= "AND cust_bill_pay._date <= $start";
5511 $end &&= "AND cust_bill_pay._date > $end";
5512 $start = '' unless defined($start);
5513 $end = '' unless defined($end);
5514 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5515 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5520 Returns an SQL fragment to retreive the amount credited against this invoice.
5525 my ($class, $start, $end) = @_;
5526 $start &&= "AND cust_credit_bill._date <= $start";
5527 $end &&= "AND cust_credit_bill._date > $end";
5528 $start = '' unless defined($start);
5529 $end = '' unless defined($end);
5530 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5531 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5536 Returns an SQL fragment to retrieve the due date of an invoice.
5537 Currently only supported on PostgreSQL.
5542 my $conf = new FS::Conf;
5546 cust_bill.invoice_terms,
5547 cust_main.invoice_terms,
5548 \''.($conf->config('invoice_default_terms') || '').'\'
5549 ), E\'Net (\\\\d+)\'
5551 ) * 86400 + cust_bill._date'
5554 =item search_sql_where HASHREF
5556 Class method which returns an SQL WHERE fragment to search for parameters
5557 specified in HASHREF. Valid parameters are
5563 List reference of start date, end date, as UNIX timestamps.
5573 List reference of charged limits (exclusive).
5577 List reference of charged limits (exclusive).
5581 flag, return open invoices only
5585 flag, return net invoices only
5589 =item newest_percust
5593 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5597 sub search_sql_where {
5598 my($class, $param) = @_;
5600 warn "$me search_sql_where called with params: \n".
5601 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5607 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5608 push @search, "cust_main.agentnum = $1";
5612 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5613 push @search, "cust_bill.custnum = $1";
5617 if ( $param->{_date} ) {
5618 my($beginning, $ending) = @{$param->{_date}};
5620 push @search, "cust_bill._date >= $beginning",
5621 "cust_bill._date < $ending";
5625 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5626 push @search, "cust_bill.invnum >= $1";
5628 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5629 push @search, "cust_bill.invnum <= $1";
5633 if ( $param->{charged} ) {
5634 my @charged = ref($param->{charged})
5635 ? @{ $param->{charged} }
5636 : ($param->{charged});
5638 push @search, map { s/^charged/cust_bill.charged/; $_; }
5642 my $owed_sql = FS::cust_bill->owed_sql;
5645 if ( $param->{owed} ) {
5646 my @owed = ref($param->{owed})
5647 ? @{ $param->{owed} }
5649 push @search, map { s/^owed/$owed_sql/; $_; }
5654 push @search, "0 != $owed_sql"
5655 if $param->{'open'};
5656 push @search, '0 != '. FS::cust_bill->net_sql
5660 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5661 if $param->{'days'};
5664 if ( $param->{'newest_percust'} ) {
5666 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5667 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5669 my @newest_where = map { my $x = $_;
5670 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5673 grep ! /^cust_main./, @search;
5674 my $newest_where = scalar(@newest_where)
5675 ? ' AND '. join(' AND ', @newest_where)
5679 push @search, "cust_bill._date = (
5680 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5681 WHERE newest_cust_bill.custnum = cust_bill.custnum
5687 #promised_date - also has an option to accept nulls
5688 if ( $param->{promised_date} ) {
5689 my($beginning, $ending, $null) = @{$param->{promised_date}};
5691 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5692 "cust_bill.promised_date < $ending )" .
5693 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5696 #agent virtualization
5697 my $curuser = $FS::CurrentUser::CurrentUser;
5698 if ( $curuser->username eq 'fs_queue'
5699 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5701 my $newuser = qsearchs('access_user', {
5702 'username' => $username,
5706 $curuser = $newuser;
5708 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5711 push @search, $curuser->agentnums_sql;
5713 join(' AND ', @search );
5725 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5726 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base