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
2014 time2str("%x", $self->_date),
2015 sprintf("%.2f", $self->charged),
2016 ( map { $cust_main->getfield($_) }
2017 qw( first last company address1 address2 city state zip country ) ),
2019 ) or die "can't create csv";
2022 my $header = $csv->string. "\n";
2025 if ( lc($opt{'format'}) eq 'billco' ) {
2028 foreach my $item ( $self->_items_pkg ) {
2031 '', # 1 | N/A-Leave Empty CHAR 2
2032 '', # 2 | N/A-Leave Empty CHAR 15
2033 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2034 $self->invnum, # 4 | Invoice Number CHAR 15
2035 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2036 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2037 $item->{'amount'}, # 7 | Amount NUM* 9
2038 '', # 8 | Line Format Control** CHAR 2
2039 '', # 9 | Grouping Code CHAR 2
2040 '', # 10 | User Defined CHAR 15
2043 $detail .= $csv->string. "\n";
2049 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2051 my($pkg, $setup, $recur, $sdate, $edate);
2052 if ( $cust_bill_pkg->pkgnum ) {
2054 ($pkg, $setup, $recur, $sdate, $edate) = (
2055 $cust_bill_pkg->part_pkg->pkg,
2056 ( $cust_bill_pkg->setup != 0
2057 ? sprintf("%.2f", $cust_bill_pkg->setup )
2059 ( $cust_bill_pkg->recur != 0
2060 ? sprintf("%.2f", $cust_bill_pkg->recur )
2062 ( $cust_bill_pkg->sdate
2063 ? time2str("%x", $cust_bill_pkg->sdate)
2065 ($cust_bill_pkg->edate
2066 ?time2str("%x", $cust_bill_pkg->edate)
2070 } else { #pkgnum tax
2071 next unless $cust_bill_pkg->setup != 0;
2072 $pkg = $cust_bill_pkg->desc;
2073 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2074 ( $sdate, $edate ) = ( '', '' );
2080 ( map { '' } (1..11) ),
2081 ($pkg, $setup, $recur, $sdate, $edate)
2082 ) or die "can't create csv";
2084 $detail .= $csv->string. "\n";
2090 ( $header, $detail );
2096 Pays this invoice with a compliemntary payment. If there is an error,
2097 returns the error, otherwise returns false.
2103 my $cust_pay = new FS::cust_pay ( {
2104 'invnum' => $self->invnum,
2105 'paid' => $self->owed,
2108 'payinfo' => $self->cust_main->payinfo,
2116 Attempts to pay this invoice with a credit card payment via a
2117 Business::OnlinePayment realtime gateway. See
2118 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2119 for supported processors.
2125 $self->realtime_bop( 'CC', @_ );
2130 Attempts to pay this invoice with an electronic check (ACH) payment via a
2131 Business::OnlinePayment realtime gateway. See
2132 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2133 for supported processors.
2139 $self->realtime_bop( 'ECHECK', @_ );
2144 Attempts to pay this invoice with phone bill (LEC) payment via a
2145 Business::OnlinePayment realtime gateway. See
2146 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2147 for supported processors.
2153 $self->realtime_bop( 'LEC', @_ );
2157 my( $self, $method ) = (shift,shift);
2158 my $conf = $self->conf;
2161 my $cust_main = $self->cust_main;
2162 my $balance = $cust_main->balance;
2163 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2164 $amount = sprintf("%.2f", $amount);
2165 return "not run (balance $balance)" unless $amount > 0;
2167 my $description = 'Internet Services';
2168 if ( $conf->exists('business-onlinepayment-description') ) {
2169 my $dtempl = $conf->config('business-onlinepayment-description');
2171 my $agent_obj = $cust_main->agent
2172 or die "can't retreive agent for $cust_main (agentnum ".
2173 $cust_main->agentnum. ")";
2174 my $agent = $agent_obj->agent;
2175 my $pkgs = join(', ',
2176 map { $_->part_pkg->pkg }
2177 grep { $_->pkgnum } $self->cust_bill_pkg
2179 $description = eval qq("$dtempl");
2182 $cust_main->realtime_bop($method, $amount,
2183 'description' => $description,
2184 'invnum' => $self->invnum,
2185 #this didn't do what we want, it just calls apply_payments_and_credits
2187 'apply_to_invoice' => 1,
2190 #this changes application behavior: auto payments
2191 #triggered against a specific invoice are now applied
2192 #to that invoice instead of oldest open.
2198 =item batch_card OPTION => VALUE...
2200 Adds a payment for this invoice to the pending credit card batch (see
2201 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2202 runs the payment using a realtime gateway.
2207 my ($self, %options) = @_;
2208 my $cust_main = $self->cust_main;
2210 $options{invnum} = $self->invnum;
2212 $cust_main->batch_card(%options);
2215 sub _agent_template {
2217 $self->cust_main->agent_template;
2220 sub _agent_invoice_from {
2222 $self->cust_main->agent_invoice_from;
2225 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2227 Returns an text invoice, as a list of lines.
2229 Options can be passed as a hashref (recommended) or as a list of time, template
2230 and then any key/value pairs for any other options.
2232 I<time>, if specified, is used to control the printing of overdue messages. The
2233 default is now. It isn't the date of the invoice; that's the `_date' field.
2234 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2235 L<Time::Local> and L<Date::Parse> for conversion functions.
2237 I<template>, if specified, is the name of a suffix for alternate invoices.
2239 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2245 my( $today, $template, %opt );
2247 %opt = %{ shift() };
2248 $today = delete($opt{'time'}) || '';
2249 $template = delete($opt{template}) || '';
2251 ( $today, $template, %opt ) = @_;
2254 my %params = ( 'format' => 'template' );
2255 $params{'time'} = $today if $today;
2256 $params{'template'} = $template if $template;
2257 $params{$_} = $opt{$_}
2258 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2260 $self->print_generic( %params );
2263 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2265 Internal method - returns a filename of a filled-in LaTeX template for this
2266 invoice (Note: add ".tex" to get the actual filename), and a filename of
2267 an associated logo (with the .eps extension included).
2269 See print_ps and print_pdf for methods that return PostScript and PDF output.
2271 Options can be passed as a hashref (recommended) or as a list of time, template
2272 and then any key/value pairs for any other options.
2274 I<time>, if specified, is used to control the printing of overdue messages. The
2275 default is now. It isn't the date of the invoice; that's the `_date' field.
2276 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2277 L<Time::Local> and L<Date::Parse> for conversion functions.
2279 I<template>, if specified, is the name of a suffix for alternate invoices.
2281 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2287 my $conf = $self->conf;
2288 my( $today, $template, %opt );
2290 %opt = %{ shift() };
2291 $today = delete($opt{'time'}) || '';
2292 $template = delete($opt{template}) || '';
2294 ( $today, $template, %opt ) = @_;
2297 my %params = ( 'format' => 'latex' );
2298 $params{'time'} = $today if $today;
2299 $params{'template'} = $template if $template;
2300 $params{$_} = $opt{$_}
2301 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2303 $template ||= $self->_agent_template;
2305 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2306 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2310 ) or die "can't open temp file: $!\n";
2312 my $agentnum = $self->cust_main->agentnum;
2314 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2315 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2316 or die "can't write temp file: $!\n";
2318 print $lh $conf->config_binary('logo.eps', $agentnum)
2319 or die "can't write temp file: $!\n";
2322 $params{'logo_file'} = $lh->filename;
2324 if($conf->exists('invoice-barcode')){
2325 my $png_file = $self->invoice_barcode($dir);
2326 my $eps_file = $png_file;
2327 $eps_file =~ s/\.png$/.eps/g;
2328 $png_file =~ /(barcode.*png)/;
2330 $eps_file =~ /(barcode.*eps)/;
2333 my $curr_dir = cwd();
2335 # after painfuly long experimentation, it was determined that sam2p won't
2336 # accept : and other chars in the path, no matter how hard I tried to
2337 # escape them, hence the chdir (and chdir back, just to be safe)
2338 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2339 or die "sam2p failed: $!\n";
2343 $params{'barcode_file'} = $eps_file;
2346 my @filled_in = $self->print_generic( %params );
2348 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2352 ) or die "can't open temp file: $!\n";
2353 binmode($fh, ':utf8'); # language support
2354 print $fh join('', @filled_in );
2357 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2358 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2362 =item invoice_barcode DIR_OR_FALSE
2364 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2365 it is taken as the temp directory where the PNG file will be generated and the
2366 PNG file name is returned. Otherwise, the PNG image itself is returned.
2370 sub invoice_barcode {
2371 my ($self, $dir) = (shift,shift);
2373 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2374 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2375 my $gd = $gdbar->plot(Height => 30);
2378 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2382 ) or die "can't open temp file: $!\n";
2383 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2384 my $png_file = $bh->filename;
2391 =item print_generic OPTION => VALUE ...
2393 Internal method - returns a filled-in template for this invoice as a scalar.
2395 See print_ps and print_pdf for methods that return PostScript and PDF output.
2397 Non optional options include
2398 format - latex, html, template
2400 Optional options include
2402 template - a value used as a suffix for a configuration template
2404 time - a value used to control the printing of overdue messages. The
2405 default is now. It isn't the date of the invoice; that's the `_date' field.
2406 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2407 L<Time::Local> and L<Date::Parse> for conversion functions.
2411 unsquelch_cdr - overrides any per customer cdr squelching when true
2413 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2415 locale - override customer's locale
2419 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2420 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2421 # yes: fixed width/plain text printing will be borked
2423 my( $self, %params ) = @_;
2424 my $conf = $self->conf;
2425 my $today = $params{today} ? $params{today} : time;
2426 warn "$me print_generic called on $self with suffix $params{template}\n"
2429 my $format = $params{format};
2430 die "Unknown format: $format"
2431 unless $format =~ /^(latex|html|template)$/;
2433 my $cust_main = $self->cust_main;
2434 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2435 unless $cust_main->payname
2436 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2438 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2439 'html' => [ '<%=', '%>' ],
2440 'template' => [ '{', '}' ],
2443 warn "$me print_generic creating template\n"
2446 #create the template
2447 my $template = $params{template} ? $params{template} : $self->_agent_template;
2448 my $templatefile = "invoice_$format";
2449 $templatefile .= "_$template"
2450 if length($template) && $conf->exists($templatefile."_$template");
2451 my @invoice_template = map "$_\n", $conf->config($templatefile)
2452 or die "cannot load config data $templatefile";
2455 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2456 #change this to a die when the old code is removed
2457 warn "old-style invoice template $templatefile; ".
2458 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2459 $old_latex = 'true';
2460 @invoice_template = _translate_old_latex_format(@invoice_template);
2463 warn "$me print_generic creating T:T object\n"
2466 my $text_template = new Text::Template(
2468 SOURCE => \@invoice_template,
2469 DELIMITERS => $delimiters{$format},
2472 warn "$me print_generic compiling T:T object\n"
2475 $text_template->compile()
2476 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2479 # additional substitution could possibly cause breakage in existing templates
2480 my %convert_maps = (
2482 'notes' => sub { map "$_", @_ },
2483 'footer' => sub { map "$_", @_ },
2484 'smallfooter' => sub { map "$_", @_ },
2485 'returnaddress' => sub { map "$_", @_ },
2486 'coupon' => sub { map "$_", @_ },
2487 'summary' => sub { map "$_", @_ },
2493 s/%%(.*)$/<!-- $1 -->/g;
2494 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2495 s/\\begin\{enumerate\}/<ol>/g;
2497 s/\\end\{enumerate\}/<\/ol>/g;
2498 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2507 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2509 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2514 s/\\\\\*?\s*$/<BR>/;
2515 s/\\hyphenation\{[\w\s\-]+}//;
2520 'coupon' => sub { "" },
2521 'summary' => sub { "" },
2528 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2529 s/\\begin\{enumerate\}//g;
2531 s/\\end\{enumerate\}//g;
2532 s/\\textbf\{(.*)\}/$1/g;
2539 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2541 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2546 s/\\\\\*?\s*$/\n/; # dubious
2547 s/\\hyphenation\{[\w\s\-]+}//;
2551 'coupon' => sub { "" },
2552 'summary' => sub { "" },
2557 # hashes for differing output formats
2558 my %nbsps = ( 'latex' => '~',
2559 'html' => '', # '&nbps;' would be nice
2560 'template' => '', # not used
2562 my $nbsp = $nbsps{$format};
2564 my %escape_functions = ( 'latex' => \&_latex_escape,
2565 'html' => \&_html_escape_nbsp,#\&encode_entities,
2566 'template' => sub { shift },
2568 my $escape_function = $escape_functions{$format};
2569 my $escape_function_nonbsp = ($format eq 'html')
2570 ? \&_html_escape : $escape_function;
2572 my %date_formats = ( 'latex' => $date_format_long,
2573 'html' => $date_format_long,
2576 $date_formats{'html'} =~ s/ / /g;
2578 my $date_format = $date_formats{$format};
2580 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2582 'html' => sub { return '<b>'. shift(). '</b>'
2584 'template' => sub { shift },
2586 my $embolden_function = $embolden_functions{$format};
2588 my %newline_tokens = ( 'latex' => '\\\\',
2592 my $newline_token = $newline_tokens{$format};
2594 warn "$me generating template variables\n"
2597 # generate template variables
2600 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2604 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2610 $returnaddress = join("\n",
2611 $conf->config_orbase("invoice_${format}returnaddress", $template)
2614 } elsif ( grep /\S/,
2615 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2617 my $convert_map = $convert_maps{$format}{'returnaddress'};
2620 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2625 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2627 my $convert_map = $convert_maps{$format}{'returnaddress'};
2628 $returnaddress = join( "\n", &$convert_map(
2629 map { s/( {2,})/'~' x length($1)/eg;
2633 ( $conf->config('company_name', $self->cust_main->agentnum),
2634 $conf->config('company_address', $self->cust_main->agentnum),
2641 my $warning = "Couldn't find a return address; ".
2642 "do you need to set the company_address configuration value?";
2644 $returnaddress = $nbsp;
2645 #$returnaddress = $warning;
2649 warn "$me generating invoice data\n"
2652 my $agentnum = $self->cust_main->agentnum;
2654 my %invoice_data = (
2657 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2658 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2659 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2660 'returnaddress' => $returnaddress,
2661 'agent' => &$escape_function($cust_main->agent->agent),
2664 'invnum' => $self->invnum,
2665 'date' => time2str($date_format, $self->_date),
2666 'today' => time2str($date_format_long, $today),
2667 'terms' => $self->terms,
2668 'template' => $template, #params{'template'},
2669 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2670 'current_charges' => sprintf("%.2f", $self->charged),
2671 'duedate' => $self->due_date2str($rdate_format), #date_format?
2674 'custnum' => $cust_main->display_custnum,
2675 'agent_custid' => &$escape_function($cust_main->agent_custid),
2676 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2677 payname company address1 address2 city state zip fax
2681 'ship_enable' => $conf->exists('invoice-ship_address'),
2682 'unitprices' => $conf->exists('invoice-unitprice'),
2683 'smallernotes' => $conf->exists('invoice-smallernotes'),
2684 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2685 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2687 #layout info -- would be fancy to calc some of this and bury the template
2689 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2690 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2691 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2692 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2693 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2694 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2695 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2696 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2697 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2698 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2700 # better hang on to conf_dir for a while (for old templates)
2701 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2703 #these are only used when doing paged plaintext
2710 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2711 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2712 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2713 # eval to avoid death for unimplemented languages
2714 my $dh = eval { Date::Language->new($info{'name'}) } ||
2715 Date::Language->new(); # fall back to English
2716 # prototype here to silence warnings
2717 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2718 # eventually use this date handle everywhere in here, too
2720 my $min_sdate = 999999999999;
2722 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2723 next unless $cust_bill_pkg->pkgnum > 0;
2724 $min_sdate = $cust_bill_pkg->sdate
2725 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2726 $max_edate = $cust_bill_pkg->edate
2727 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2730 $invoice_data{'bill_period'} = '';
2731 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2732 . " to " . time2str('%e %h', $max_edate)
2733 if ($max_edate != 0 && $min_sdate != 999999999999);
2735 $invoice_data{finance_section} = '';
2736 if ( $conf->config('finance_pkgclass') ) {
2738 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2739 $invoice_data{finance_section} = $pkg_class->categoryname;
2741 $invoice_data{finance_amount} = '0.00';
2742 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2744 my $countrydefault = $conf->config('countrydefault') || 'US';
2745 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2746 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2747 my $method = $prefix.$_;
2748 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2750 $invoice_data{'ship_country'} = ''
2751 if ( $invoice_data{'ship_country'} eq $countrydefault );
2753 $invoice_data{'cid'} = $params{'cid'}
2756 if ( $cust_main->country eq $countrydefault ) {
2757 $invoice_data{'country'} = '';
2759 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2763 $invoice_data{'address'} = \@address;
2765 $cust_main->payname.
2766 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2767 ? " (P.O. #". $cust_main->payinfo. ")"
2771 push @address, $cust_main->company
2772 if $cust_main->company;
2773 push @address, $cust_main->address1;
2774 push @address, $cust_main->address2
2775 if $cust_main->address2;
2777 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2778 push @address, $invoice_data{'country'}
2779 if $invoice_data{'country'};
2781 while (scalar(@address) < 5);
2783 $invoice_data{'logo_file'} = $params{'logo_file'}
2784 if $params{'logo_file'};
2785 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2786 if $params{'barcode_file'};
2787 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2788 if $params{'barcode_img'};
2789 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2790 if $params{'barcode_cid'};
2792 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2793 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2794 #my $balance_due = $self->owed + $pr_total - $cr_total;
2795 my $balance_due = $self->owed + $pr_total;
2797 # the customer's current balance as shown on the invoice before this one
2798 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2800 # the change in balance from that invoice to this one
2801 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2803 # the sum of amount owed on all previous invoices
2804 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2806 # the sum of amount owed on all invoices
2807 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2809 # info from customer's last invoice before this one, for some
2811 $invoice_data{'last_bill'} = {};
2812 my $last_bill = $pr_cust_bill[-1];
2814 $invoice_data{'last_bill'} = {
2815 '_date' => $last_bill->_date, #unformatted
2816 # all we need for now
2820 my $summarypage = '';
2821 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2824 $invoice_data{'summarypage'} = $summarypage;
2826 warn "$me substituting variables in notes, footer, smallfooter\n"
2829 my @include = (qw( notes footer smallfooter ));
2830 push @include, 'coupon' unless $params{'no_coupon'};
2831 foreach my $include (@include) {
2833 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2836 if ( $conf->exists($inc_file, $agentnum)
2837 && length( $conf->config($inc_file, $agentnum) ) ) {
2839 @inc_src = $conf->config($inc_file, $agentnum);
2843 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2845 my $convert_map = $convert_maps{$format}{$include};
2847 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2848 s/--\@\]/$delimiters{$format}[1]/g;
2851 &$convert_map( $conf->config($inc_file, $agentnum) );
2855 my $inc_tt = new Text::Template (
2857 SOURCE => [ map "$_\n", @inc_src ],
2858 DELIMITERS => $delimiters{$format},
2859 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2861 unless ( $inc_tt->compile() ) {
2862 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2863 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2867 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2869 $invoice_data{$include} =~ s/\n+$//
2870 if ($format eq 'latex');
2873 # let invoices use either of these as needed
2874 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2875 ? $cust_main->payinfo : '';
2876 $invoice_data{'po_line'} =
2877 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2878 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2881 my %money_chars = ( 'latex' => '',
2882 'html' => $conf->config('money_char') || '$',
2885 my $money_char = $money_chars{$format};
2887 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2888 'html' => $conf->config('money_char') || '$',
2891 my $other_money_char = $other_money_chars{$format};
2892 $invoice_data{'dollar'} = $other_money_char;
2894 my @detail_items = ();
2895 my @total_items = ();
2899 $invoice_data{'detail_items'} = \@detail_items;
2900 $invoice_data{'total_items'} = \@total_items;
2901 $invoice_data{'buf'} = \@buf;
2902 $invoice_data{'sections'} = \@sections;
2904 warn "$me generating sections\n"
2907 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2908 'subtotal' => $other_money_char.
2909 sprintf('%.2f', $pr_total),
2910 'summarized' => '', #why? $summarypage ? 'Y' : '',
2912 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2913 join(' / ', map { $cust_main->balance_date_range(@$_) }
2914 $self->_prior_month30s
2916 if $conf->exists('invoice_include_aging');
2919 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2920 'subtotal' => $taxtotal, # adjusted below
2922 my $tax_weight = _pkg_category($tax_section->{description})
2923 ? _pkg_category($tax_section->{description})->weight
2925 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2926 $tax_section->{'sort_weight'} = $tax_weight;
2929 my $adjusttotal = 0;
2930 my $adjust_section = { 'description' =>
2931 $self->mt('Credits, Payments, and Adjustments'),
2932 'subtotal' => 0, # adjusted below
2934 my $adjust_weight = _pkg_category($adjust_section->{description})
2935 ? _pkg_category($adjust_section->{description})->weight
2937 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2938 $adjust_section->{'sort_weight'} = $adjust_weight;
2940 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2941 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2942 $invoice_data{'multisection'} = $multisection;
2943 my $late_sections = [];
2944 my $extra_sections = [];
2945 my $extra_lines = ();
2946 if ( $multisection ) {
2947 ($extra_sections, $extra_lines) =
2948 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2949 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2951 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2953 push @detail_items, @$extra_lines if $extra_lines;
2955 $self->_items_sections( $late_sections, # this could stand a refactor
2957 $escape_function_nonbsp,
2961 if ($conf->exists('svc_phone_sections')) {
2962 my ($phone_sections, $phone_lines) =
2963 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2964 push @{$late_sections}, @$phone_sections;
2965 push @detail_items, @$phone_lines;
2967 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2968 my ($accountcode_section, $accountcode_lines) =
2969 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2970 if ( scalar(@$accountcode_lines) ) {
2971 push @{$late_sections}, $accountcode_section;
2972 push @detail_items, @$accountcode_lines;
2975 } else {# not multisection
2976 # make a default section
2977 push @sections, { 'description' => '', 'subtotal' => '',
2978 'no_subtotal' => 1 };
2979 # and calculate the finance charge total, since it won't get done otherwise.
2980 # XXX possibly other totals?
2981 # XXX possibly finance_pkgclass should not be used in this manner?
2982 if ( $conf->exists('finance_pkgclass') ) {
2983 my @finance_charges;
2984 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2985 if ( grep { $_->section eq $invoice_data{finance_section} }
2986 $cust_bill_pkg->cust_bill_pkg_display ) {
2987 # I think these are always setup fees, but just to be sure...
2988 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2991 $invoice_data{finance_amount} =
2992 sprintf('%.2f', sum( @finance_charges ) || 0);
2996 unless ( $conf->exists('disable_previous_balance', $agentnum)
2997 || $conf->exists('previous_balance-summary_only')
3001 warn "$me adding previous balances\n"
3004 foreach my $line_item ( $self->_items_previous ) {
3007 ext_description => [],
3009 $detail->{'ref'} = $line_item->{'pkgnum'};
3010 $detail->{'quantity'} = 1;
3011 $detail->{'section'} = $previous_section;
3012 $detail->{'description'} = &$escape_function($line_item->{'description'});
3013 if ( exists $line_item->{'ext_description'} ) {
3014 @{$detail->{'ext_description'}} = map {
3015 &$escape_function($_);
3016 } @{$line_item->{'ext_description'}};
3018 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3019 $line_item->{'amount'};
3020 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3022 push @detail_items, $detail;
3023 push @buf, [ $detail->{'description'},
3024 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3030 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3032 push @buf, ['','-----------'];
3033 push @buf, [ $self->mt('Total Previous Balance'),
3034 $money_char. sprintf("%10.2f", $pr_total) ];
3038 if ( $conf->exists('svc_phone-did-summary') ) {
3039 warn "$me adding DID summary\n"
3042 my ($didsummary,$minutes) = $self->_did_summary;
3043 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3045 { 'description' => $didsummary_desc,
3046 'ext_description' => [ $didsummary, $minutes ],
3050 foreach my $section (@sections, @$late_sections) {
3052 warn "$me adding section \n". Dumper($section)
3055 # begin some normalization
3056 $section->{'subtotal'} = $section->{'amount'}
3058 && !exists($section->{subtotal})
3059 && exists($section->{amount});
3061 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3062 if ( $invoice_data{finance_section} &&
3063 $section->{'description'} eq $invoice_data{finance_section} );
3065 $section->{'subtotal'} = $other_money_char.
3066 sprintf('%.2f', $section->{'subtotal'})
3069 # continue some normalization
3070 $section->{'amount'} = $section->{'subtotal'}
3074 if ( $section->{'description'} ) {
3075 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3080 warn "$me setting options\n"
3083 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3085 $options{'section'} = $section if $multisection;
3086 $options{'format'} = $format;
3087 $options{'escape_function'} = $escape_function;
3088 $options{'no_usage'} = 1 unless $unsquelched;
3089 $options{'unsquelched'} = $unsquelched;
3090 $options{'summary_page'} = $summarypage;
3091 $options{'skip_usage'} =
3092 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3093 $options{'multilocation'} = $multilocation;
3094 $options{'multisection'} = $multisection;
3096 warn "$me searching for line items\n"
3099 foreach my $line_item ( $self->_items_pkg(%options) ) {
3101 warn "$me adding line item $line_item\n"
3105 ext_description => [],
3107 $detail->{'ref'} = $line_item->{'pkgnum'};
3108 $detail->{'quantity'} = $line_item->{'quantity'};
3109 $detail->{'section'} = $section;
3110 $detail->{'description'} = &$escape_function($line_item->{'description'});
3111 if ( exists $line_item->{'ext_description'} ) {
3112 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3114 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3115 $line_item->{'amount'};
3116 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3117 $line_item->{'unit_amount'};
3118 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3120 $detail->{'sdate'} = $line_item->{'sdate'};
3121 $detail->{'edate'} = $line_item->{'edate'};
3122 $detail->{'seconds'} = $line_item->{'seconds'};
3124 push @detail_items, $detail;
3125 push @buf, ( [ $detail->{'description'},
3126 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3128 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3132 if ( $section->{'description'} ) {
3133 push @buf, ( ['','-----------'],
3134 [ $section->{'description'}. ' sub-total',
3135 $section->{'subtotal'} # already formatted this
3144 $invoice_data{current_less_finance} =
3145 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3147 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3148 || $conf->exists('previous_balance-summary_only') )
3150 unshift @sections, $previous_section if $pr_total;
3153 warn "$me adding taxes\n"
3156 foreach my $tax ( $self->_items_tax ) {
3158 $taxtotal += $tax->{'amount'};
3160 my $description = &$escape_function( $tax->{'description'} );
3161 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3163 if ( $multisection ) {
3165 my $money = $old_latex ? '' : $money_char;
3166 push @detail_items, {
3167 ext_description => [],
3170 description => $description,
3171 amount => $money. $amount,
3173 section => $tax_section,
3178 push @total_items, {
3179 'total_item' => $description,
3180 'total_amount' => $other_money_char. $amount,
3185 push @buf,[ $description,
3186 $money_char. $amount,
3193 $total->{'total_item'} = $self->mt('Sub-total');
3194 $total->{'total_amount'} =
3195 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3197 if ( $multisection ) {
3198 $tax_section->{'subtotal'} = $other_money_char.
3199 sprintf('%.2f', $taxtotal);
3200 $tax_section->{'pretotal'} = 'New charges sub-total '.
3201 $total->{'total_amount'};
3202 push @sections, $tax_section if $taxtotal;
3204 unshift @total_items, $total;
3207 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3209 push @buf,['','-----------'];
3210 push @buf,[$self->mt(
3211 $conf->exists('disable_previous_balance', $agentnum)
3213 : 'Total New Charges'
3215 $money_char. sprintf("%10.2f",$self->charged) ];
3221 $item = $conf->config('previous_balance-exclude_from_total')
3222 || 'Total New Charges'
3223 if $conf->exists('previous_balance-exclude_from_total');
3224 my $amount = $self->charged +
3225 ( $conf->exists('disable_previous_balance', $agentnum) ||
3226 $conf->exists('previous_balance-exclude_from_total')
3230 $total->{'total_item'} = &$embolden_function($self->mt($item));
3231 $total->{'total_amount'} =
3232 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3233 if ( $multisection ) {
3234 if ( $adjust_section->{'sort_weight'} ) {
3235 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3236 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3238 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3239 $other_money_char. sprintf('%.2f', $self->charged );
3242 push @total_items, $total;
3244 push @buf,['','-----------'];
3247 sprintf( '%10.2f', $amount )
3252 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3253 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3256 my $credittotal = 0;
3257 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3260 $total->{'total_item'} = &$escape_function($credit->{'description'});
3261 $credittotal += $credit->{'amount'};
3262 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3263 $adjusttotal += $credit->{'amount'};
3264 if ( $multisection ) {
3265 my $money = $old_latex ? '' : $money_char;
3266 push @detail_items, {
3267 ext_description => [],
3270 description => &$escape_function($credit->{'description'}),
3271 amount => $money. $credit->{'amount'},
3273 section => $adjust_section,
3276 push @total_items, $total;
3280 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3283 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3284 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3288 my $paymenttotal = 0;
3289 foreach my $payment ( $self->_items_payments ) {
3291 $total->{'total_item'} = &$escape_function($payment->{'description'});
3292 $paymenttotal += $payment->{'amount'};
3293 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3294 $adjusttotal += $payment->{'amount'};
3295 if ( $multisection ) {
3296 my $money = $old_latex ? '' : $money_char;
3297 push @detail_items, {
3298 ext_description => [],
3301 description => &$escape_function($payment->{'description'}),
3302 amount => $money. $payment->{'amount'},
3304 section => $adjust_section,
3307 push @total_items, $total;
3309 push @buf, [ $payment->{'description'},
3310 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3313 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3315 if ( $multisection ) {
3316 $adjust_section->{'subtotal'} = $other_money_char.
3317 sprintf('%.2f', $adjusttotal);
3318 push @sections, $adjust_section
3319 unless $adjust_section->{sort_weight};
3322 # create Balance Due message
3325 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3326 $total->{'total_amount'} =
3327 &$embolden_function(
3328 $other_money_char. sprintf('%.2f', $summarypage
3330 $self->billing_balance
3331 : $self->owed + $pr_total
3334 if ( $multisection && !$adjust_section->{sort_weight} ) {
3335 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3336 $total->{'total_amount'};
3338 push @total_items, $total;
3340 push @buf,['','-----------'];
3341 push @buf,[$self->balance_due_msg, $money_char.
3342 sprintf("%10.2f", $balance_due ) ];
3345 if ( $conf->exists('previous_balance-show_credit')
3346 and $cust_main->balance < 0 ) {
3347 my $credit_total = {
3348 'total_item' => &$embolden_function($self->credit_balance_msg),
3349 'total_amount' => &$embolden_function(
3350 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3353 if ( $multisection ) {
3354 $adjust_section->{'posttotal'} .= $newline_token .
3355 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3358 push @total_items, $credit_total;
3360 push @buf,['','-----------'];
3361 push @buf,[$self->credit_balance_msg, $money_char.
3362 sprintf("%10.2f", -$cust_main->balance ) ];
3366 if ( $multisection ) {
3367 if ($conf->exists('svc_phone_sections')) {
3369 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3370 $total->{'total_amount'} =
3371 &$embolden_function(
3372 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3374 my $last_section = pop @sections;
3375 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3376 $total->{'total_amount'};
3377 push @sections, $last_section;
3379 push @sections, @$late_sections
3383 # make a discounts-available section, even without multisection
3384 if ( $conf->exists('discount-show_available')
3385 and my @discounts_avail = $self->_items_discounts_avail ) {
3386 my $discount_section = {
3387 'description' => $self->mt('Discounts Available'),
3392 push @sections, $discount_section;
3393 push @detail_items, map { +{
3394 'ref' => '', #should this be something else?
3395 'section' => $discount_section,
3396 'description' => &$escape_function( $_->{description} ),
3397 'amount' => $money_char . &$escape_function( $_->{amount} ),
3398 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3399 } } @discounts_avail;
3402 # All sections and items are built; now fill in templates.
3403 my @includelist = ();
3404 push @includelist, 'summary' if $summarypage;
3405 foreach my $include ( @includelist ) {
3407 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3410 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3412 @inc_src = $conf->config($inc_file, $agentnum);
3416 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3418 my $convert_map = $convert_maps{$format}{$include};
3420 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3421 s/--\@\]/$delimiters{$format}[1]/g;
3424 &$convert_map( $conf->config($inc_file, $agentnum) );
3428 my $inc_tt = new Text::Template (
3430 SOURCE => [ map "$_\n", @inc_src ],
3431 DELIMITERS => $delimiters{$format},
3432 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3434 unless ( $inc_tt->compile() ) {
3435 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3436 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3440 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3442 $invoice_data{$include} =~ s/\n+$//
3443 if ($format eq 'latex');
3448 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3449 /invoice_lines\((\d*)\)/;
3450 $invoice_lines += $1 || scalar(@buf);
3453 die "no invoice_lines() functions in template?"
3454 if ( $format eq 'template' && !$wasfunc );
3456 if ($format eq 'template') {
3458 if ( $invoice_lines ) {
3459 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3460 $invoice_data{'total_pages'}++
3461 if scalar(@buf) % $invoice_lines;
3464 #setup subroutine for the template
3465 $invoice_data{invoice_lines} = sub {
3466 my $lines = shift || scalar(@buf);
3478 push @collect, split("\n",
3479 $text_template->fill_in( HASH => \%invoice_data )
3481 $invoice_data{'page'}++;
3483 map "$_\n", @collect;
3485 # this is where we actually create the invoice
3486 warn "filling in template for invoice ". $self->invnum. "\n"
3488 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3491 $text_template->fill_in(HASH => \%invoice_data);
3495 # helper routine for generating date ranges
3496 sub _prior_month30s {
3499 [ 1, 2592000 ], # 0-30 days ago
3500 [ 2592000, 5184000 ], # 30-60 days ago
3501 [ 5184000, 7776000 ], # 60-90 days ago
3502 [ 7776000, 0 ], # 90+ days ago
3505 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3506 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3511 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3513 Returns an postscript invoice, as a scalar.
3515 Options can be passed as a hashref (recommended) or as a list of time, template
3516 and then any key/value pairs for any other options.
3518 I<time> an optional value used to control the printing of overdue messages. The
3519 default is now. It isn't the date of the invoice; that's the `_date' field.
3520 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3521 L<Time::Local> and L<Date::Parse> for conversion functions.
3523 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3530 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3531 my $ps = generate_ps($file);
3533 unlink($barcodefile) if $barcodefile;
3538 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3540 Returns an PDF invoice, as a scalar.
3542 Options can be passed as a hashref (recommended) or as a list of time, template
3543 and then any key/value pairs for any other options.
3545 I<time> an optional value used to control the printing of overdue messages. The
3546 default is now. It isn't the date of the invoice; that's the `_date' field.
3547 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3548 L<Time::Local> and L<Date::Parse> for conversion functions.
3550 I<template>, if specified, is the name of a suffix for alternate invoices.
3552 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3559 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3560 my $pdf = generate_pdf($file);
3562 unlink($barcodefile) if $barcodefile;
3567 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3569 Returns an HTML invoice, as a scalar.
3571 I<time> an optional value used to control the printing of overdue messages. The
3572 default is now. It isn't the date of the invoice; that's the `_date' field.
3573 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3574 L<Time::Local> and L<Date::Parse> for conversion functions.
3576 I<template>, if specified, is the name of a suffix for alternate invoices.
3578 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3580 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3581 when emailing the invoice as part of a multipart/related MIME email.
3589 %params = %{ shift() };
3591 $params{'time'} = shift;
3592 $params{'template'} = shift;
3593 $params{'cid'} = shift;
3596 $params{'format'} = 'html';
3598 $self->print_generic( %params );
3601 # quick subroutine for print_latex
3603 # There are ten characters that LaTeX treats as special characters, which
3604 # means that they do not simply typeset themselves:
3605 # # $ % & ~ _ ^ \ { }
3607 # TeX ignores blanks following an escaped character; if you want a blank (as
3608 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3612 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3613 $value =~ s/([<>])/\$$1\$/g;
3619 encode_entities($value);
3623 sub _html_escape_nbsp {
3624 my $value = _html_escape(shift);
3625 $value =~ s/ +/ /g;
3629 #utility methods for print_*
3631 sub _translate_old_latex_format {
3632 warn "_translate_old_latex_format called\n"
3639 if ( $line =~ /^%%Detail\s*$/ ) {
3641 push @template, q![@--!,
3642 q! foreach my $_tr_line (@detail_items) {!,
3643 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3644 q! $_tr_line->{'description'} .= !,
3645 q! "\\tabularnewline\n~~".!,
3646 q! join( "\\tabularnewline\n~~",!,
3647 q! @{$_tr_line->{'ext_description'}}!,
3651 while ( ( my $line_item_line = shift )
3652 !~ /^%%EndDetail\s*$/ ) {
3653 $line_item_line =~ s/'/\\'/g; # nice LTS
3654 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3655 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3656 push @template, " \$OUT .= '$line_item_line';";
3659 push @template, '}',
3662 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3664 push @template, '[@--',
3665 ' foreach my $_tr_line (@total_items) {';
3667 while ( ( my $total_item_line = shift )
3668 !~ /^%%EndTotalDetails\s*$/ ) {
3669 $total_item_line =~ s/'/\\'/g; # nice LTS
3670 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3671 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3672 push @template, " \$OUT .= '$total_item_line';";
3675 push @template, '}',
3679 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3680 push @template, $line;
3686 warn "$_\n" foreach @template;
3694 my $conf = $self->conf;
3696 #check for an invoice-specific override
3697 return $self->invoice_terms if $self->invoice_terms;
3699 #check for a customer- specific override
3700 my $cust_main = $self->cust_main;
3701 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3703 #use configured default
3704 $conf->config('invoice_default_terms') || '';
3710 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3711 $duedate = $self->_date() + ( $1 * 86400 );
3718 $self->due_date ? time2str(shift, $self->due_date) : '';
3721 sub balance_due_msg {
3723 my $msg = $self->mt('Balance Due');
3724 return $msg unless $self->terms;
3725 if ( $self->due_date ) {
3726 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3727 $self->due_date2str($date_format);
3728 } elsif ( $self->terms ) {
3729 $msg .= ' - '. $self->terms;
3734 sub balance_due_date {
3736 my $conf = $self->conf;
3738 if ( $conf->exists('invoice_default_terms')
3739 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3740 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3745 sub credit_balance_msg {
3747 $self->mt('Credit Balance Remaining')
3750 =item invnum_date_pretty
3752 Returns a string with the invoice number and date, for example:
3753 "Invoice #54 (3/20/2008)"
3757 sub invnum_date_pretty {
3759 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3764 Returns a string with the date, for example: "3/20/2008"
3770 time2str($date_format, $self->_date);
3773 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3775 Generate section information for all items appearing on this invoice.
3776 This will only be called for multi-section invoices.
3778 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3779 related display records (L<FS::cust_bill_pkg_display>) and organize
3780 them into two groups ("early" and "late" according to whether they come
3781 before or after the total), then into sections. A subtotal is calculated
3784 Section descriptions are returned in sort weight order. Each consists
3785 of a hash containing:
3787 description: the package category name, escaped
3788 subtotal: the total charges in that section
3789 tax_section: a flag indicating that the section contains only tax charges
3790 summarized: same as tax_section, for some reason
3791 sort_weight: the package category's sort weight
3793 If 'condense' is set on the display record, it also contains everything
3794 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3795 coderefs to generate parts of the invoice. This is not advised.
3799 LATE: an arrayref to push the "late" section hashes onto. The "early"
3800 group is simply returned from the method.
3802 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3803 Turning this on has the following effects:
3804 - Ignores display items with the 'summary' flag.
3805 - Combines all items into the "early" group.
3806 - Creates sections for all non-disabled package categories, even if they
3807 have no charges on this invoice, as well as a section with no name.
3809 ESCAPE: an escape function to use for section titles.
3811 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3812 sorted list. If there are any of these, section subtotals exclude
3815 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3816 passed through to C<_condense_section()>.
3820 use vars qw(%pkg_category_cache);
3821 sub _items_sections {
3824 my $summarypage = shift;
3826 my $extra_sections = shift;
3830 my %late_subtotal = ();
3833 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3836 my $usage = $cust_bill_pkg->usage;
3838 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3839 next if ( $display->summary && $summarypage );
3841 my $section = $display->section;
3842 my $type = $display->type;
3844 $not_tax{$section} = 1
3845 unless $cust_bill_pkg->pkgnum == 0;
3847 if ( $display->post_total && !$summarypage ) {
3848 if (! $type || $type eq 'S') {
3849 $late_subtotal{$section} += $cust_bill_pkg->setup
3850 if $cust_bill_pkg->setup != 0;
3854 $late_subtotal{$section} += $cust_bill_pkg->recur
3855 if $cust_bill_pkg->recur != 0;
3858 if ($type && $type eq 'R') {
3859 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3860 if $cust_bill_pkg->recur != 0;
3863 if ($type && $type eq 'U') {
3864 $late_subtotal{$section} += $usage
3865 unless scalar(@$extra_sections);
3870 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3872 if (! $type || $type eq 'S') {
3873 $subtotal{$section} += $cust_bill_pkg->setup
3874 if $cust_bill_pkg->setup != 0;
3878 $subtotal{$section} += $cust_bill_pkg->recur
3879 if $cust_bill_pkg->recur != 0;
3882 if ($type && $type eq 'R') {
3883 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3884 if $cust_bill_pkg->recur != 0;
3887 if ($type && $type eq 'U') {
3888 $subtotal{$section} += $usage
3889 unless scalar(@$extra_sections);
3898 %pkg_category_cache = ();
3900 push @$late, map { { 'description' => &{$escape}($_),
3901 'subtotal' => $late_subtotal{$_},
3903 'sort_weight' => ( _pkg_category($_)
3904 ? _pkg_category($_)->weight
3907 ((_pkg_category($_) && _pkg_category($_)->condense)
3908 ? $self->_condense_section($format)
3912 sort _sectionsort keys %late_subtotal;
3915 if ( $summarypage ) {
3916 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3917 map { $_->categoryname } qsearch('pkg_category', {});
3918 push @sections, '' if exists($subtotal{''});
3920 @sections = keys %subtotal;
3923 my @early = map { { 'description' => &{$escape}($_),
3924 'subtotal' => $subtotal{$_},
3925 'summarized' => $not_tax{$_} ? '' : 'Y',
3926 'tax_section' => $not_tax{$_} ? '' : 'Y',
3927 'sort_weight' => ( _pkg_category($_)
3928 ? _pkg_category($_)->weight
3931 ((_pkg_category($_) && _pkg_category($_)->condense)
3932 ? $self->_condense_section($format)
3937 push @early, @$extra_sections if $extra_sections;
3939 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3943 #helper subs for above
3946 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3950 my $categoryname = shift;
3951 $pkg_category_cache{$categoryname} ||=
3952 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3955 my %condensed_format = (
3956 'label' => [ qw( Description Qty Amount ) ],
3958 sub { shift->{description} },
3959 sub { shift->{quantity} },
3960 sub { my($href, %opt) = @_;
3961 ($opt{dollar} || ''). $href->{amount};
3964 'align' => [ qw( l r r ) ],
3965 'span' => [ qw( 5 1 1 ) ], # unitprices?
3966 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3969 sub _condense_section {
3970 my ( $self, $format ) = ( shift, shift );
3972 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3973 qw( description_generator
3976 total_line_generator
3981 sub _condensed_generator_defaults {
3982 my ( $self, $format ) = ( shift, shift );
3983 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3992 sub _condensed_header_generator {
3993 my ( $self, $format ) = ( shift, shift );
3995 my ( $f, $prefix, $suffix, $separator, $column ) =
3996 _condensed_generator_defaults($format);
3998 if ($format eq 'latex') {
3999 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4000 $suffix = "\\\\\n\\hline";
4003 sub { my ($d,$a,$s,$w) = @_;
4004 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4006 } elsif ( $format eq 'html' ) {
4007 $prefix = '<th></th>';
4011 sub { my ($d,$a,$s,$w) = @_;
4012 return qq!<th align="$html_align{$a}">$d</th>!;
4020 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4022 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4025 $prefix. join($separator, @result). $suffix;
4030 sub _condensed_description_generator {
4031 my ( $self, $format ) = ( shift, shift );
4033 my ( $f, $prefix, $suffix, $separator, $column ) =
4034 _condensed_generator_defaults($format);
4036 my $money_char = '$';
4037 if ($format eq 'latex') {
4038 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4040 $separator = " & \n";
4042 sub { my ($d,$a,$s,$w) = @_;
4043 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4045 $money_char = '\\dollar';
4046 }elsif ( $format eq 'html' ) {
4047 $prefix = '"><td align="center"></td>';
4051 sub { my ($d,$a,$s,$w) = @_;
4052 return qq!<td align="$html_align{$a}">$d</td>!;
4054 #$money_char = $conf->config('money_char') || '$';
4055 $money_char = ''; # this is madness
4063 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4065 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4067 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4068 map { $f->{$_}->[$i] } qw(align span width)
4072 $prefix. join( $separator, @result ). $suffix;
4077 sub _condensed_total_generator {
4078 my ( $self, $format ) = ( shift, shift );
4080 my ( $f, $prefix, $suffix, $separator, $column ) =
4081 _condensed_generator_defaults($format);
4084 if ($format eq 'latex') {
4087 $separator = " & \n";
4089 sub { my ($d,$a,$s,$w) = @_;
4090 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4092 }elsif ( $format eq 'html' ) {
4096 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4098 sub { my ($d,$a,$s,$w) = @_;
4099 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4108 # my $r = &{$f->{fields}->[$i]}(@args);
4109 # $r .= ' Total' unless $i;
4111 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4113 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4114 map { $f->{$_}->[$i] } qw(align span width)
4118 $prefix. join( $separator, @result ). $suffix;
4123 =item total_line_generator FORMAT
4125 Returns a coderef used for generation of invoice total line items for this
4126 usage_class. FORMAT is either html or latex
4130 # should not be used: will have issues with hash element names (description vs
4131 # total_item and amount vs total_amount -- another array of functions?
4133 sub _condensed_total_line_generator {
4134 my ( $self, $format ) = ( shift, shift );
4136 my ( $f, $prefix, $suffix, $separator, $column ) =
4137 _condensed_generator_defaults($format);
4140 if ($format eq 'latex') {
4143 $separator = " & \n";
4145 sub { my ($d,$a,$s,$w) = @_;
4146 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4148 }elsif ( $format eq 'html' ) {
4152 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4154 sub { my ($d,$a,$s,$w) = @_;
4155 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4164 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4166 &{$column}( &{$f->{fields}->[$i]}(@args),
4167 map { $f->{$_}->[$i] } qw(align span width)
4171 $prefix. join( $separator, @result ). $suffix;
4176 #sub _items_extra_usage_sections {
4178 # my $escape = shift;
4180 # my %sections = ();
4182 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4183 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4185 # next unless $cust_bill_pkg->pkgnum > 0;
4187 # foreach my $section ( keys %usage_class ) {
4189 # my $usage = $cust_bill_pkg->usage($section);
4191 # next unless $usage && $usage > 0;
4193 # $sections{$section} ||= 0;
4194 # $sections{$section} += $usage;
4200 # map { { 'description' => &{$escape}($_),
4201 # 'subtotal' => $sections{$_},
4202 # 'summarized' => '',
4203 # 'tax_section' => '',
4206 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4210 sub _items_extra_usage_sections {
4212 my $conf = $self->conf;
4220 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4222 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4223 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4224 next unless $cust_bill_pkg->pkgnum > 0;
4226 foreach my $classnum ( keys %usage_class ) {
4227 my $section = $usage_class{$classnum}->classname;
4228 $classnums{$section} = $classnum;
4230 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4231 my $amount = $detail->amount;
4232 next unless $amount && $amount > 0;
4234 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4235 $sections{$section}{amount} += $amount; #subtotal
4236 $sections{$section}{calls}++;
4237 $sections{$section}{duration} += $detail->duration;
4239 my $desc = $detail->regionname;
4240 my $description = $desc;
4241 $description = substr($desc, 0, $maxlength). '...'
4242 if $format eq 'latex' && length($desc) > $maxlength;
4244 $lines{$section}{$desc} ||= {
4245 description => &{$escape}($description),
4246 #pkgpart => $part_pkg->pkgpart,
4247 pkgnum => $cust_bill_pkg->pkgnum,
4252 #unit_amount => $cust_bill_pkg->unitrecur,
4253 quantity => $cust_bill_pkg->quantity,
4254 product_code => 'N/A',
4255 ext_description => [],
4258 $lines{$section}{$desc}{amount} += $amount;
4259 $lines{$section}{$desc}{calls}++;
4260 $lines{$section}{$desc}{duration} += $detail->duration;
4266 my %sectionmap = ();
4267 foreach (keys %sections) {
4268 my $usage_class = $usage_class{$classnums{$_}};
4269 $sectionmap{$_} = { 'description' => &{$escape}($_),
4270 'amount' => $sections{$_}{amount}, #subtotal
4271 'calls' => $sections{$_}{calls},
4272 'duration' => $sections{$_}{duration},
4274 'tax_section' => '',
4275 'sort_weight' => $usage_class->weight,
4276 ( $usage_class->format
4277 ? ( map { $_ => $usage_class->$_($format) }
4278 qw( description_generator header_generator total_generator total_line_generator )
4285 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4289 foreach my $section ( keys %lines ) {
4290 foreach my $line ( keys %{$lines{$section}} ) {
4291 my $l = $lines{$section}{$line};
4292 $l->{section} = $sectionmap{$section};
4293 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4294 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4299 return(\@sections, \@lines);
4305 my $end = $self->_date;
4307 # start at date of previous invoice + 1 second or 0 if no previous invoice
4308 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4309 $start = 0 if !$start;
4312 my $cust_main = $self->cust_main;
4313 my @pkgs = $cust_main->all_pkgs;
4314 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4317 foreach my $pkg ( @pkgs ) {
4318 my @h_cust_svc = $pkg->h_cust_svc($end);
4319 foreach my $h_cust_svc ( @h_cust_svc ) {
4320 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4321 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4323 my $inserted = $h_cust_svc->date_inserted;
4324 my $deleted = $h_cust_svc->date_deleted;
4325 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4327 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4329 # DID either activated or ported in; cannot be both for same DID simultaneously
4330 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4331 && (!$phone_inserted->lnp_status
4332 || $phone_inserted->lnp_status eq ''
4333 || $phone_inserted->lnp_status eq 'native')) {
4336 else { # this one not so clean, should probably move to (h_)svc_phone
4337 my $phone_portedin = qsearchs( 'h_svc_phone',
4338 { 'svcnum' => $h_cust_svc->svcnum,
4339 'lnp_status' => 'portedin' },
4340 FS::h_svc_phone->sql_h_searchs($end),
4342 $num_portedin++ if $phone_portedin;
4345 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4346 if($deleted >= $start && $deleted <= $end && $phone_deleted
4347 && (!$phone_deleted->lnp_status
4348 || $phone_deleted->lnp_status ne 'portingout')) {
4351 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4352 && $phone_deleted->lnp_status
4353 && $phone_deleted->lnp_status eq 'portingout') {
4357 # increment usage minutes
4358 if ( $phone_inserted ) {
4359 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4360 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4363 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4366 # don't look at this service again
4367 push @seen, $h_cust_svc->svcnum;
4371 $minutes = sprintf("%d", $minutes);
4372 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4373 . "$num_deactivated Ported-Out: $num_portedout ",
4374 "Total Minutes: $minutes");
4377 sub _items_accountcode_cdr {
4382 my $section = { 'amount' => 0,
4385 'sort_weight' => '',
4387 'description' => 'Usage by Account Code',
4393 my %accountcodes = ();
4395 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4396 next unless $cust_bill_pkg->pkgnum > 0;
4398 my @header = $cust_bill_pkg->details_header;
4399 next unless scalar(@header);
4400 $section->{'header'} = join(',',@header);
4402 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4404 $section->{'header'} = $detail->formatted('format' => $format)
4405 if($detail->detail eq $section->{'header'});
4407 my $accountcode = $detail->accountcode;
4408 next unless $accountcode;
4410 my $amount = $detail->amount;
4411 next unless $amount && $amount > 0;
4413 $accountcodes{$accountcode} ||= {
4414 description => $accountcode,
4421 product_code => 'N/A',
4422 section => $section,
4423 ext_description => [ $section->{'header'} ],
4427 $section->{'amount'} += $amount;
4428 $accountcodes{$accountcode}{'amount'} += $amount;
4429 $accountcodes{$accountcode}{calls}++;
4430 $accountcodes{$accountcode}{duration} += $detail->duration;
4431 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4435 foreach my $l ( values %accountcodes ) {
4436 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4437 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4438 foreach my $sorted_detail ( @sorted_detail ) {
4439 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4441 delete $l->{detail_temp};
4445 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4447 return ($section,\@sorted_lines);
4450 sub _items_svc_phone_sections {
4452 my $conf = $self->conf;
4460 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4462 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4463 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4465 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4466 next unless $cust_bill_pkg->pkgnum > 0;
4468 my @header = $cust_bill_pkg->details_header;
4469 next unless scalar(@header);
4471 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4473 my $phonenum = $detail->phonenum;
4474 next unless $phonenum;
4476 my $amount = $detail->amount;
4477 next unless $amount && $amount > 0;
4479 $sections{$phonenum} ||= { 'amount' => 0,
4482 'sort_weight' => -1,
4483 'phonenum' => $phonenum,
4485 $sections{$phonenum}{amount} += $amount; #subtotal
4486 $sections{$phonenum}{calls}++;
4487 $sections{$phonenum}{duration} += $detail->duration;
4489 my $desc = $detail->regionname;
4490 my $description = $desc;
4491 $description = substr($desc, 0, $maxlength). '...'
4492 if $format eq 'latex' && length($desc) > $maxlength;
4494 $lines{$phonenum}{$desc} ||= {
4495 description => &{$escape}($description),
4496 #pkgpart => $part_pkg->pkgpart,
4504 product_code => 'N/A',
4505 ext_description => [],
4508 $lines{$phonenum}{$desc}{amount} += $amount;
4509 $lines{$phonenum}{$desc}{calls}++;
4510 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4512 my $line = $usage_class{$detail->classnum}->classname;
4513 $sections{"$phonenum $line"} ||=
4517 'sort_weight' => $usage_class{$detail->classnum}->weight,
4518 'phonenum' => $phonenum,
4519 'header' => [ @header ],
4521 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4522 $sections{"$phonenum $line"}{calls}++;
4523 $sections{"$phonenum $line"}{duration} += $detail->duration;
4525 $lines{"$phonenum $line"}{$desc} ||= {
4526 description => &{$escape}($description),
4527 #pkgpart => $part_pkg->pkgpart,
4535 product_code => 'N/A',
4536 ext_description => [],
4539 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4540 $lines{"$phonenum $line"}{$desc}{calls}++;
4541 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4542 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4543 $detail->formatted('format' => $format);
4548 my %sectionmap = ();
4549 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4550 foreach ( keys %sections ) {
4551 my @header = @{ $sections{$_}{header} || [] };
4553 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4554 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4555 my $usage_class = $summary ? $simple : $usage_simple;
4556 my $ending = $summary ? ' usage charges' : '';
4559 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4561 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4562 'amount' => $sections{$_}{amount}, #subtotal
4563 'calls' => $sections{$_}{calls},
4564 'duration' => $sections{$_}{duration},
4566 'tax_section' => '',
4567 'phonenum' => $sections{$_}{phonenum},
4568 'sort_weight' => $sections{$_}{sort_weight},
4569 'post_total' => $summary, #inspire pagebreak
4571 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4572 qw( description_generator
4575 total_line_generator
4582 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4583 $a->{sort_weight} <=> $b->{sort_weight}
4588 foreach my $section ( keys %lines ) {
4589 foreach my $line ( keys %{$lines{$section}} ) {
4590 my $l = $lines{$section}{$line};
4591 $l->{section} = $sectionmap{$section};
4592 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4593 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4598 if($conf->exists('phone_usage_class_summary')) {
4599 # this only works with Latex
4603 # after this, we'll have only two sections per DID:
4604 # Calls Summary and Calls Detail
4605 foreach my $section ( @sections ) {
4606 if($section->{'post_total'}) {
4607 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4608 $section->{'total_line_generator'} = sub { '' };
4609 $section->{'total_generator'} = sub { '' };
4610 $section->{'header_generator'} = sub { '' };
4611 $section->{'description_generator'} = '';
4612 push @newsections, $section;
4613 my %calls_detail = %$section;
4614 $calls_detail{'post_total'} = '';
4615 $calls_detail{'sort_weight'} = '';
4616 $calls_detail{'description_generator'} = sub { '' };
4617 $calls_detail{'header_generator'} = sub {
4618 return ' & Date/Time & Called Number & Duration & Price'
4619 if $format eq 'latex';
4622 $calls_detail{'description'} = 'Calls Detail: '
4623 . $section->{'phonenum'};
4624 push @newsections, \%calls_detail;
4628 # after this, each usage class is collapsed/summarized into a single
4629 # line under the Calls Summary section
4630 foreach my $newsection ( @newsections ) {
4631 if($newsection->{'post_total'}) { # this means Calls Summary
4632 foreach my $section ( @sections ) {
4633 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4634 && !$section->{'post_total'});
4635 my $newdesc = $section->{'description'};
4636 my $tn = $section->{'phonenum'};
4637 $newdesc =~ s/$tn//g;
4638 my $line = { ext_description => [],
4642 calls => $section->{'calls'},
4643 section => $newsection,
4644 duration => $section->{'duration'},
4645 description => $newdesc,
4646 amount => sprintf("%.2f",$section->{'amount'}),
4647 product_code => 'N/A',
4649 push @newlines, $line;
4654 # after this, Calls Details is populated with all CDRs
4655 foreach my $newsection ( @newsections ) {
4656 if(!$newsection->{'post_total'}) { # this means Calls Details
4657 foreach my $line ( @lines ) {
4658 next unless (scalar(@{$line->{'ext_description'}}) &&
4659 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4661 my @extdesc = @{$line->{'ext_description'}};
4663 foreach my $extdesc ( @extdesc ) {
4664 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4665 push @newextdesc, $extdesc;
4667 $line->{'ext_description'} = \@newextdesc;
4668 $line->{'section'} = $newsection;
4669 push @newlines, $line;
4674 return(\@newsections, \@newlines);
4677 return(\@sections, \@lines);
4681 sub _items { # seems to be unused
4684 #my @display = scalar(@_)
4686 # : qw( _items_previous _items_pkg );
4687 # #: qw( _items_pkg );
4688 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4689 my @display = qw( _items_previous _items_pkg );
4692 foreach my $display ( @display ) {
4693 push @b, $self->$display(@_);
4698 sub _items_previous {
4700 my $conf = $self->conf;
4701 my $cust_main = $self->cust_main;
4702 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4704 foreach ( @pr_cust_bill ) {
4705 my $date = $conf->exists('invoice_show_prior_due_date')
4706 ? 'due '. $_->due_date2str($date_format)
4707 : time2str($date_format, $_->_date);
4709 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4710 #'pkgpart' => 'N/A',
4712 'amount' => sprintf("%.2f", $_->owed),
4718 # 'description' => 'Previous Balance',
4719 # #'pkgpart' => 'N/A',
4720 # 'pkgnum' => 'N/A',
4721 # 'amount' => sprintf("%10.2f", $pr_total ),
4722 # 'ext_description' => [ map {
4723 # "Invoice ". $_->invnum.
4724 # " (". time2str("%x",$_->_date). ") ".
4725 # sprintf("%10.2f", $_->owed)
4726 # } @pr_cust_bill ],
4731 =item _items_pkg [ OPTIONS ]
4733 Return line item hashes for each package item on this invoice. Nearly
4736 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4738 The only OPTIONS accepted is 'section', which may point to a hashref
4739 with a key named 'condensed', which may have a true value. If it
4740 does, this method tries to merge identical items into items with
4741 'quantity' equal to the number of items (not the sum of their
4742 separate quantities, for some reason).
4750 warn "$me _items_pkg searching for all package line items\n"
4753 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4755 warn "$me _items_pkg filtering line items\n"
4757 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4759 if ($options{section} && $options{section}->{condensed}) {
4761 warn "$me _items_pkg condensing section\n"
4765 local $Storable::canonical = 1;
4766 foreach ( @items ) {
4768 delete $item->{ref};
4769 delete $item->{ext_description};
4770 my $key = freeze($item);
4771 $itemshash{$key} ||= 0;
4772 $itemshash{$key} ++; # += $item->{quantity};
4774 @items = sort { $a->{description} cmp $b->{description} }
4775 map { my $i = thaw($_);
4776 $i->{quantity} = $itemshash{$_};
4778 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4784 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4791 return 0 unless $a->itemdesc cmp $b->itemdesc;
4792 return -1 if $b->itemdesc eq 'Tax';
4793 return 1 if $a->itemdesc eq 'Tax';
4794 return -1 if $b->itemdesc eq 'Other surcharges';
4795 return 1 if $a->itemdesc eq 'Other surcharges';
4796 $a->itemdesc cmp $b->itemdesc;
4801 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4802 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4805 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4807 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4808 list of hashrefs describing the line items they generate on the invoice.
4810 OPTIONS may include:
4812 format: the invoice format.
4814 escape_function: the function used to escape strings.
4816 DEPRECATED? (expensive, mostly unused?)
4817 format_function: the function used to format CDRs.
4819 section: a hashref containing 'description'; if this is present,
4820 cust_bill_pkg_display records not belonging to this section are
4823 multisection: a flag indicating that this is a multisection invoice,
4824 which does something complicated.
4826 multilocation: a flag to display the location label for the package.
4828 Returns a list of hashrefs, each of which may contain:
4830 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4831 ext_description, which is an arrayref of detail lines to show below
4836 sub _items_cust_bill_pkg {
4838 my $conf = $self->conf;
4839 my $cust_bill_pkgs = shift;
4842 my $format = $opt{format} || '';
4843 my $escape_function = $opt{escape_function} || sub { shift };
4844 my $format_function = $opt{format_function} || '';
4845 my $no_usage = $opt{no_usage} || '';
4846 my $unsquelched = $opt{unsquelched} || ''; #unused
4847 my $section = $opt{section}->{description} if $opt{section};
4848 my $summary_page = $opt{summary_page} || ''; #unused
4849 my $multilocation = $opt{multilocation} || '';
4850 my $multisection = $opt{multisection} || '';
4851 my $discount_show_always = 0;
4853 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4856 my ($s, $r, $u) = ( undef, undef, undef );
4857 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4860 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4861 if ( $_ && !$cust_bill_pkg->hidden ) {
4862 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4863 $_->{amount} =~ s/^\-0\.00$/0.00/;
4864 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4866 if $_->{amount} != 0
4867 || $discount_show_always
4868 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4869 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4875 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4876 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4879 foreach my $display ( grep { defined($section)
4880 ? $_->section eq $section
4883 #grep { !$_->summary || !$summary_page } # bunk!
4884 grep { !$_->summary || $multisection }
4885 $cust_bill_pkg->cust_bill_pkg_display
4889 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4890 $display->billpkgdisplaynum. "\n"
4893 my $type = $display->type;
4895 my $desc = $cust_bill_pkg->desc;
4896 $desc = substr($desc, 0, $maxlength). '...'
4897 if $format eq 'latex' && length($desc) > $maxlength;
4899 my %details_opt = ( 'format' => $format,
4900 'escape_function' => $escape_function,
4901 'format_function' => $format_function,
4902 'no_usage' => $opt{'no_usage'},
4905 if ( $cust_bill_pkg->pkgnum > 0 ) {
4907 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4910 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4912 # start/end dates for invoice formats that do nonstandard
4914 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4916 if ( (!$type || $type eq 'S')
4917 && ( $cust_bill_pkg->setup != 0
4918 || $cust_bill_pkg->setup_show_zero
4923 warn "$me _items_cust_bill_pkg adding setup\n"
4926 my $description = $desc;
4927 $description .= ' Setup'
4928 if $cust_bill_pkg->recur != 0
4929 || $discount_show_always
4930 || $cust_bill_pkg->recur_show_zero;
4933 unless ( $cust_pkg->part_pkg->hide_svc_detail
4934 || $cust_bill_pkg->hidden )
4937 push @d, map &{$escape_function}($_),
4938 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4939 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4941 if ( $multilocation ) {
4942 my $loc = $cust_pkg->location_label;
4943 $loc = substr($loc, 0, $maxlength). '...'
4944 if $format eq 'latex' && length($loc) > $maxlength;
4945 push @d, &{$escape_function}($loc);
4948 } #unless hiding service details
4950 push @d, $cust_bill_pkg->details(%details_opt)
4951 if $cust_bill_pkg->recur == 0;
4953 if ( $cust_bill_pkg->hidden ) {
4954 $s->{amount} += $cust_bill_pkg->setup;
4955 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4956 push @{ $s->{ext_description} }, @d;
4960 description => $description,
4961 #pkgpart => $part_pkg->pkgpart,
4962 pkgnum => $cust_bill_pkg->pkgnum,
4963 amount => $cust_bill_pkg->setup,
4964 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4965 unit_amount => $cust_bill_pkg->unitsetup,
4966 quantity => $cust_bill_pkg->quantity,
4967 ext_description => \@d,
4973 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4975 $cust_bill_pkg->recur != 0
4976 || $cust_bill_pkg->setup == 0
4977 || $discount_show_always
4978 || $cust_bill_pkg->recur_show_zero
4983 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4986 my $is_summary = $display->summary;
4987 my $description = ($is_summary && $type && $type eq 'U')
4988 ? "Usage charges" : $desc;
4991 $conf->exists('disable_line_item_date_ranges')
4992 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
4995 my $date_style = $conf->config('cust_bill-line_item-date_style');
4996 if ( defined($date_style) && $date_style eq 'month_of' ) {
4997 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
4999 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5000 " - ". time2str($date_format, $cust_bill_pkg->edate);
5002 $description .= " ($time_period)";
5006 my @seconds = (); # for display of usage info
5008 #at least until cust_bill_pkg has "past" ranges in addition to
5009 #the "future" sdate/edate ones... see #3032
5010 my @dates = ( $self->_date );
5011 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5012 push @dates, $prev->sdate if $prev;
5013 push @dates, undef if !$prev;
5015 unless ( $cust_pkg->part_pkg->hide_svc_detail
5016 || $cust_bill_pkg->itemdesc
5017 || $cust_bill_pkg->hidden
5018 || $is_summary && $type && $type eq 'U' )
5021 warn "$me _items_cust_bill_pkg adding service details\n"
5024 push @d, map &{$escape_function}($_),
5025 $cust_pkg->h_labels_short(@dates, 'I')
5026 #$cust_bill_pkg->edate,
5027 #$cust_bill_pkg->sdate)
5028 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5030 warn "$me _items_cust_bill_pkg done adding service details\n"
5033 if ( $multilocation ) {
5034 my $loc = $cust_pkg->location_label;
5035 $loc = substr($loc, 0, $maxlength). '...'
5036 if $format eq 'latex' && length($loc) > $maxlength;
5037 push @d, &{$escape_function}($loc);
5040 # Display of seconds_since_sqlradacct:
5041 # On the invoice, when processing @detail_items, look for a field
5042 # named 'seconds'. This will contain total seconds for each
5043 # service, in the same order as @ext_description. For services
5044 # that don't support this it will show undef.
5045 if ( $conf->exists('svc_acct-usage_seconds')
5046 and ! $cust_bill_pkg->pkgpart_override ) {
5047 foreach my $cust_svc (
5048 $cust_pkg->h_cust_svc(@dates, 'I')
5051 # eval because not having any part_export_usage exports
5052 # is a fatal error, last_bill/_date because that's how
5053 # sqlradius_hour billing does it
5055 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5057 push @seconds, $sec;
5059 } #if svc_acct-usage_seconds
5063 unless ( $is_summary ) {
5064 warn "$me _items_cust_bill_pkg adding details\n"
5067 #instead of omitting details entirely in this case (unwanted side
5068 # effects), just omit CDRs
5069 $details_opt{'no_usage'} = 1
5070 if $type && $type eq 'R';
5072 push @d, $cust_bill_pkg->details(%details_opt);
5075 warn "$me _items_cust_bill_pkg calculating amount\n"
5080 $amount = $cust_bill_pkg->recur;
5081 } elsif ($type eq 'R') {
5082 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5083 } elsif ($type eq 'U') {
5084 $amount = $cust_bill_pkg->usage;
5087 if ( !$type || $type eq 'R' ) {
5089 warn "$me _items_cust_bill_pkg adding recur\n"
5092 if ( $cust_bill_pkg->hidden ) {
5093 $r->{amount} += $amount;
5094 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5095 push @{ $r->{ext_description} }, @d;
5098 description => $description,
5099 #pkgpart => $part_pkg->pkgpart,
5100 pkgnum => $cust_bill_pkg->pkgnum,
5102 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5103 unit_amount => $cust_bill_pkg->unitrecur,
5104 quantity => $cust_bill_pkg->quantity,
5106 ext_description => \@d,
5108 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5111 } else { # $type eq 'U'
5113 warn "$me _items_cust_bill_pkg adding usage\n"
5116 if ( $cust_bill_pkg->hidden ) {
5117 $u->{amount} += $amount;
5118 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5119 push @{ $u->{ext_description} }, @d;
5122 description => $description,
5123 #pkgpart => $part_pkg->pkgpart,
5124 pkgnum => $cust_bill_pkg->pkgnum,
5126 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5127 unit_amount => $cust_bill_pkg->unitrecur,
5128 quantity => $cust_bill_pkg->quantity,
5130 ext_description => \@d,
5135 } # recurring or usage with recurring charge
5137 } else { #pkgnum tax or one-shot line item (??)
5139 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5142 if ( $cust_bill_pkg->setup != 0 ) {
5144 'description' => $desc,
5145 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5148 if ( $cust_bill_pkg->recur != 0 ) {
5150 'description' => "$desc (".
5151 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5152 time2str($date_format, $cust_bill_pkg->edate). ')',
5153 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5161 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5162 && $conf->exists('discount-show-always'));
5166 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5168 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5169 $_->{amount} =~ s/^\-0\.00$/0.00/;
5170 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5172 if $_->{amount} != 0
5173 || $discount_show_always
5174 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5175 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5179 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5186 sub _items_credits {
5187 my( $self, %opt ) = @_;
5188 my $trim_len = $opt{'trim_len'} || 60;
5192 foreach ( $self->cust_credited ) {
5194 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5196 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5197 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5198 $reason = " ($reason) " if $reason;
5201 #'description' => 'Credit ref\#'. $_->crednum.
5202 # " (". time2str("%x",$_->cust_credit->_date) .")".
5204 'description' => $self->mt('Credit applied').' '.
5205 time2str($date_format,$_->cust_credit->_date). $reason,
5206 'amount' => sprintf("%.2f",$_->amount),
5214 sub _items_payments {
5218 #get & print payments
5219 foreach ( $self->cust_bill_pay ) {
5221 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5224 'description' => $self->mt('Payment received').' '.
5225 time2str($date_format,$_->cust_pay->_date ),
5226 'amount' => sprintf("%.2f", $_->amount )
5234 =item _items_discounts_avail
5236 Returns an array of line item hashrefs representing available term discounts
5237 for this invoice. This makes the same assumptions that apply to term
5238 discounts in general: that the package is billed monthly, at a flat rate,
5239 with no usage charges. A prorated first month will be handled, as will
5240 a setup fee if the discount is allowed to apply to setup fees.
5244 sub _items_discounts_avail {
5246 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5248 my %plans = $self->discount_plans;
5250 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5254 my $plan = $plans{$months};
5256 my $term_total = sprintf('%.2f', $plan->discounted_total);
5257 my $percent = sprintf('%.0f',
5258 100 * (1 - $term_total / $plan->base_total) );
5259 my $permonth = sprintf('%.2f', $term_total / $months);
5260 my $detail = $self->mt('discount on item'). ' '.
5261 join(', ', map { "#$_" } $plan->pkgnums)
5265 description => $self->mt('Save [_1]% by paying for [_2] months',
5267 amount => $self->mt('[_1] ([_2] per month)',
5268 $term_total, $money_char.$permonth),
5269 ext_description => ($detail || ''),
5272 sort { $b <=> $a } keys %plans;
5276 =item call_details [ OPTION => VALUE ... ]
5278 Returns an array of CSV strings representing the call details for this invoice
5279 The only option available is the boolean prepend_billed_number
5284 my ($self, %opt) = @_;
5286 my $format_function = sub { shift };
5288 if ($opt{prepend_billed_number}) {
5289 $format_function = sub {
5293 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5298 my @details = map { $_->details( 'format_function' => $format_function,
5299 'escape_function' => sub{ return() },
5303 $self->cust_bill_pkg;
5304 my $header = $details[0];
5305 ( $header, grep { $_ ne $header } @details );
5315 =item process_reprint
5319 sub process_reprint {
5320 process_re_X('print', @_);
5323 =item process_reemail
5327 sub process_reemail {
5328 process_re_X('email', @_);
5336 process_re_X('fax', @_);
5344 process_re_X('ftp', @_);
5351 sub process_respool {
5352 process_re_X('spool', @_);
5355 use Storable qw(thaw);
5359 my( $method, $job ) = ( shift, shift );
5360 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5362 my $param = thaw(decode_base64(shift));
5363 warn Dumper($param) if $DEBUG;
5374 my($method, $job, %param ) = @_;
5376 warn "re_X $method for job $job with param:\n".
5377 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5380 #some false laziness w/search/cust_bill.html
5382 my $orderby = 'ORDER BY cust_bill._date';
5384 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5386 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5388 my @cust_bill = qsearch( {
5389 #'select' => "cust_bill.*",
5390 'table' => 'cust_bill',
5391 'addl_from' => $addl_from,
5393 'extra_sql' => $extra_sql,
5394 'order_by' => $orderby,
5398 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5400 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5403 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5404 foreach my $cust_bill ( @cust_bill ) {
5405 $cust_bill->$method();
5407 if ( $job ) { #progressbar foo
5409 if ( time - $min_sec > $last ) {
5410 my $error = $job->update_statustext(
5411 int( 100 * $num / scalar(@cust_bill) )
5413 die $error if $error;
5424 =head1 CLASS METHODS
5430 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5435 my ($class, $start, $end) = @_;
5437 $class->paid_sql($start, $end). ' - '.
5438 $class->credited_sql($start, $end);
5443 Returns an SQL fragment to retreive the net amount (charged minus credited).
5448 my ($class, $start, $end) = @_;
5449 'charged - '. $class->credited_sql($start, $end);
5454 Returns an SQL fragment to retreive the amount paid against this invoice.
5459 my ($class, $start, $end) = @_;
5460 $start &&= "AND cust_bill_pay._date <= $start";
5461 $end &&= "AND cust_bill_pay._date > $end";
5462 $start = '' unless defined($start);
5463 $end = '' unless defined($end);
5464 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5465 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5470 Returns an SQL fragment to retreive the amount credited against this invoice.
5475 my ($class, $start, $end) = @_;
5476 $start &&= "AND cust_credit_bill._date <= $start";
5477 $end &&= "AND cust_credit_bill._date > $end";
5478 $start = '' unless defined($start);
5479 $end = '' unless defined($end);
5480 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5481 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5486 Returns an SQL fragment to retrieve the due date of an invoice.
5487 Currently only supported on PostgreSQL.
5492 my $conf = new FS::Conf;
5496 cust_bill.invoice_terms,
5497 cust_main.invoice_terms,
5498 \''.($conf->config('invoice_default_terms') || '').'\'
5499 ), E\'Net (\\\\d+)\'
5501 ) * 86400 + cust_bill._date'
5504 =item search_sql_where HASHREF
5506 Class method which returns an SQL WHERE fragment to search for parameters
5507 specified in HASHREF. Valid parameters are
5513 List reference of start date, end date, as UNIX timestamps.
5523 List reference of charged limits (exclusive).
5527 List reference of charged limits (exclusive).
5531 flag, return open invoices only
5535 flag, return net invoices only
5539 =item newest_percust
5543 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5547 sub search_sql_where {
5548 my($class, $param) = @_;
5550 warn "$me search_sql_where called with params: \n".
5551 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5557 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5558 push @search, "cust_main.agentnum = $1";
5562 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5563 push @search, "cust_bill.custnum = $1";
5567 if ( $param->{_date} ) {
5568 my($beginning, $ending) = @{$param->{_date}};
5570 push @search, "cust_bill._date >= $beginning",
5571 "cust_bill._date < $ending";
5575 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5576 push @search, "cust_bill.invnum >= $1";
5578 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5579 push @search, "cust_bill.invnum <= $1";
5583 if ( $param->{charged} ) {
5584 my @charged = ref($param->{charged})
5585 ? @{ $param->{charged} }
5586 : ($param->{charged});
5588 push @search, map { s/^charged/cust_bill.charged/; $_; }
5592 my $owed_sql = FS::cust_bill->owed_sql;
5595 if ( $param->{owed} ) {
5596 my @owed = ref($param->{owed})
5597 ? @{ $param->{owed} }
5599 push @search, map { s/^owed/$owed_sql/; $_; }
5604 push @search, "0 != $owed_sql"
5605 if $param->{'open'};
5606 push @search, '0 != '. FS::cust_bill->net_sql
5610 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5611 if $param->{'days'};
5614 if ( $param->{'newest_percust'} ) {
5616 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5617 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5619 my @newest_where = map { my $x = $_;
5620 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5623 grep ! /^cust_main./, @search;
5624 my $newest_where = scalar(@newest_where)
5625 ? ' AND '. join(' AND ', @newest_where)
5629 push @search, "cust_bill._date = (
5630 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5631 WHERE newest_cust_bill.custnum = cust_bill.custnum
5637 #promised_date - also has an option to accept nulls
5638 if ( $param->{promised_date} ) {
5639 my($beginning, $ending, $null) = @{$param->{promised_date}};
5641 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5642 "cust_bill.promised_date < $ending )" .
5643 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5646 #agent virtualization
5647 my $curuser = $FS::CurrentUser::CurrentUser;
5648 if ( $curuser->username eq 'fs_queue'
5649 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5651 my $newuser = qsearchs('access_user', {
5652 'username' => $username,
5656 $curuser = $newuser;
5658 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5661 push @search, $curuser->agentnums_sql;
5663 join(' AND ', @search );
5675 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5676 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base