4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
147 =item promised_date - customer promised payment date, for collection
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
165 sub cust_linked { $_[0]->cust_main_custnum; }
166 sub cust_unlinked_msg {
168 "WARNING: can't find cust_main.custnum ". $self->custnum.
169 ' (cust_bill.invnum '. $self->invnum. ')';
174 Adds this invoice to the database ("Posts" the invoice). If there is an error,
175 returns the error, otherwise returns false.
181 warn "$me insert called\n" if $DEBUG;
183 local $SIG{HUP} = 'IGNORE';
184 local $SIG{INT} = 'IGNORE';
185 local $SIG{QUIT} = 'IGNORE';
186 local $SIG{TERM} = 'IGNORE';
187 local $SIG{TSTP} = 'IGNORE';
188 local $SIG{PIPE} = 'IGNORE';
190 my $oldAutoCommit = $FS::UID::AutoCommit;
191 local $FS::UID::AutoCommit = 0;
194 my $error = $self->SUPER::insert;
196 $dbh->rollback if $oldAutoCommit;
200 if ( $self->get('cust_bill_pkg') ) {
201 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202 $cust_bill_pkg->invnum($self->invnum);
203 my $error = $cust_bill_pkg->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "can't create invoice line item: $error";
211 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 This method now works but you probably shouldn't use it. Instead, apply a
219 credit against the invoice.
221 Using this method to delete invoices outright is really, really bad. There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
225 Really, don't use it.
231 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
256 foreach my $linked ( $self->$table() ) {
257 my $error = $linked->delete;
259 $dbh->rollback if $oldAutoCommit;
266 my $error = $self->SUPER::delete(@_);
268 $dbh->rollback if $oldAutoCommit;
272 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 =item replace [ OLD_RECORD ]
280 You can, but probably shouldn't modify invoices...
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record. If there is an error, returns the error,
284 otherwise returns false.
288 #replace can be inherited from Record.pm
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
294 my( $new, $old ) = ( shift, shift );
295 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296 #return "Can't change _date!" unless $old->_date eq $new->_date;
297 return "Can't change _date" unless $old->_date == $new->_date;
298 return "Can't change charged" unless $old->charged == $new->charged
299 || $old->charged == 0
300 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
306 =item add_cc_surcharge
312 sub add_cc_surcharge {
313 my ($self, $pkgnum, $amount) = (shift, shift, shift);
316 my $cust_bill_pkg = new FS::cust_bill_pkg({
317 'invnum' => $self->invnum,
321 $error = $cust_bill_pkg->insert;
322 return $error if $error;
324 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325 $self->charged($self->charged+$amount);
326 $error = $self->replace;
327 return $error if $error;
329 $self->apply_payments_and_credits;
335 Checks all fields to make sure this is a valid invoice. If there is an error,
336 returns the error, otherwise returns false. Called by the insert and replace
345 $self->ut_numbern('invnum')
346 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347 || $self->ut_numbern('_date')
348 || $self->ut_money('charged')
349 || $self->ut_numbern('printed')
350 || $self->ut_enum('closed', [ '', 'Y' ])
351 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352 || $self->ut_numbern('agent_invid') #varchar?
354 return $error if $error;
356 $self->_date(time) unless $self->_date;
358 $self->printed(0) if $self->printed eq '';
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
372 my $conf = $self->conf;
373 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374 return $self->agent_invid;
376 return $self->invnum;
382 Returns a list consisting of the total previous balance for this customer,
383 followed by the previous outstanding invoices (as FS::cust_bill objects also).
390 my @cust_bill = sort { $a->_date <=> $b->_date }
391 grep { $_->owed != 0 && $_->_date < $self->_date }
392 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
394 foreach ( @cust_bill ) { $total += $_->owed; }
400 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
407 { 'table' => 'cust_bill_pkg',
408 'hashref' => { 'invnum' => $self->invnum },
409 'order_by' => 'ORDER BY billpkgnum',
414 =item cust_bill_pkg_pkgnum PKGNUM
416 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
421 sub cust_bill_pkg_pkgnum {
422 my( $self, $pkgnum ) = @_;
424 { 'table' => 'cust_bill_pkg',
425 'hashref' => { 'invnum' => $self->invnum,
428 'order_by' => 'ORDER BY billpkgnum',
435 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
442 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
443 $self->cust_bill_pkg;
445 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
450 Returns true if any of the packages (or their definitions) corresponding to the
451 line items for this invoice have the no_auto flag set.
457 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
460 =item open_cust_bill_pkg
462 Returns the open line items for this invoice.
464 Note that cust_bill_pkg with both setup and recur fees are returned as two
465 separate line items, each with only one fee.
469 # modeled after cust_main::open_cust_bill
470 sub open_cust_bill_pkg {
473 # grep { $_->owed > 0 } $self->cust_bill_pkg
475 my %other = ( 'recur' => 'setup',
476 'setup' => 'recur', );
478 foreach my $field ( qw( recur setup )) {
479 push @open, map { $_->set( $other{$field}, 0 ); $_; }
480 grep { $_->owed($field) > 0 }
481 $self->cust_bill_pkg;
487 =item cust_bill_event
489 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
493 sub cust_bill_event {
495 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
498 =item num_cust_bill_event
500 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
504 sub num_cust_bill_event {
507 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
508 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
509 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
510 $sth->fetchrow_arrayref->[0];
515 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
519 #false laziness w/cust_pkg.pm
523 'table' => 'cust_event',
524 'addl_from' => 'JOIN part_event USING ( eventpart )',
525 'hashref' => { 'tablenum' => $self->invnum },
526 'extra_sql' => " AND eventtable = 'cust_bill' ",
532 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
536 #false laziness w/cust_pkg.pm
540 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
541 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
542 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
543 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
544 $sth->fetchrow_arrayref->[0];
549 Returns the customer (see L<FS::cust_main>) for this invoice.
555 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
558 =item cust_suspend_if_balance_over AMOUNT
560 Suspends the customer associated with this invoice if the total amount owed on
561 this invoice and all older invoices is greater than the specified amount.
563 Returns a list: an empty list on success or a list of errors.
567 sub cust_suspend_if_balance_over {
568 my( $self, $amount ) = ( shift, shift );
569 my $cust_main = $self->cust_main;
570 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
573 $cust_main->suspend(@_);
579 Depreciated. See the cust_credited method.
581 #Returns a list consisting of the total previous credited (see
582 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
583 #outstanding credits (FS::cust_credit objects).
589 croak "FS::cust_bill->cust_credit depreciated; see ".
590 "FS::cust_bill->cust_credit_bill";
593 #my @cust_credit = sort { $a->_date <=> $b->_date }
594 # grep { $_->credited != 0 && $_->_date < $self->_date }
595 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
597 #foreach (@cust_credit) { $total += $_->credited; }
598 #$total, @cust_credit;
603 Depreciated. See the cust_bill_pay method.
605 #Returns all payments (see L<FS::cust_pay>) for this invoice.
611 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
613 #sort { $a->_date <=> $b->_date }
614 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
620 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
623 sub cust_bill_pay_batch {
625 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
630 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
636 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
637 sort { $a->_date <=> $b->_date }
638 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
643 =item cust_credit_bill
645 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
651 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
652 sort { $a->_date <=> $b->_date }
653 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
657 sub cust_credit_bill {
658 shift->cust_credited(@_);
661 #=item cust_bill_pay_pkgnum PKGNUM
663 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
664 #with matching pkgnum.
668 #sub cust_bill_pay_pkgnum {
669 # my( $self, $pkgnum ) = @_;
670 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
671 # sort { $a->_date <=> $b->_date }
672 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
673 # 'pkgnum' => $pkgnum,
678 =item cust_bill_pay_pkg PKGNUM
680 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
681 applied against the matching pkgnum.
685 sub cust_bill_pay_pkg {
686 my( $self, $pkgnum ) = @_;
689 'select' => 'cust_bill_pay_pkg.*',
690 'table' => 'cust_bill_pay_pkg',
691 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
692 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
693 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
694 " AND cust_bill_pkg.pkgnum = $pkgnum",
699 #=item cust_credited_pkgnum PKGNUM
701 #=item cust_credit_bill_pkgnum PKGNUM
703 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
704 #with matching pkgnum.
708 #sub cust_credited_pkgnum {
709 # my( $self, $pkgnum ) = @_;
710 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
711 # sort { $a->_date <=> $b->_date }
712 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
713 # 'pkgnum' => $pkgnum,
718 #sub cust_credit_bill_pkgnum {
719 # shift->cust_credited_pkgnum(@_);
722 =item cust_credit_bill_pkg PKGNUM
724 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
725 applied against the matching pkgnum.
729 sub cust_credit_bill_pkg {
730 my( $self, $pkgnum ) = @_;
733 'select' => 'cust_credit_bill_pkg.*',
734 'table' => 'cust_credit_bill_pkg',
735 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
736 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
737 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
738 " AND cust_bill_pkg.pkgnum = $pkgnum",
743 =item cust_bill_batch
745 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
749 sub cust_bill_batch {
751 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
756 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
757 hash keyed by term length.
763 FS::discount_plan->all($self);
768 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
775 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
777 foreach (@taxlines) { $total += $_->setup; }
783 Returns the amount owed (still outstanding) on this invoice, which is charged
784 minus all payment applications (see L<FS::cust_bill_pay>) and credit
785 applications (see L<FS::cust_credit_bill>).
791 my $balance = $self->charged;
792 $balance -= $_->amount foreach ( $self->cust_bill_pay );
793 $balance -= $_->amount foreach ( $self->cust_credited );
794 $balance = sprintf( "%.2f", $balance);
795 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
800 my( $self, $pkgnum ) = @_;
802 #my $balance = $self->charged;
804 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
806 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
807 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
809 $balance = sprintf( "%.2f", $balance);
810 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
816 Returns true if this invoice should be hidden. See the
817 selfservice-hide_invoices-taxclass configuraiton setting.
823 my $conf = $self->conf;
824 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
826 my @cust_bill_pkg = $self->cust_bill_pkg;
827 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
828 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
831 =item apply_payments_and_credits [ OPTION => VALUE ... ]
833 Applies unapplied payments and credits to this invoice.
835 A hash of optional arguments may be passed. Currently "manual" is supported.
836 If true, a payment receipt is sent instead of a statement when
837 'payment_receipt_email' configuration option is set.
839 If there is an error, returns the error, otherwise returns false.
843 sub apply_payments_and_credits {
844 my( $self, %options ) = @_;
845 my $conf = $self->conf;
847 local $SIG{HUP} = 'IGNORE';
848 local $SIG{INT} = 'IGNORE';
849 local $SIG{QUIT} = 'IGNORE';
850 local $SIG{TERM} = 'IGNORE';
851 local $SIG{TSTP} = 'IGNORE';
852 local $SIG{PIPE} = 'IGNORE';
854 my $oldAutoCommit = $FS::UID::AutoCommit;
855 local $FS::UID::AutoCommit = 0;
858 $self->select_for_update; #mutex
860 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
861 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
863 if ( $conf->exists('pkg-balances') ) {
864 # limit @payments & @credits to those w/ a pkgnum grepped from $self
865 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
866 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
867 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
870 while ( $self->owed > 0 and ( @payments || @credits ) ) {
873 if ( @payments && @credits ) {
875 #decide which goes first by weight of top (unapplied) line item
877 my @open_lineitems = $self->open_cust_bill_pkg;
880 max( map { $_->part_pkg->pay_weight || 0 }
885 my $max_credit_weight =
886 max( map { $_->part_pkg->credit_weight || 0 }
892 #if both are the same... payments first? it has to be something
893 if ( $max_pay_weight >= $max_credit_weight ) {
899 } elsif ( @payments ) {
901 } elsif ( @credits ) {
904 die "guru meditation #12 and 35";
908 if ( $app eq 'pay' ) {
910 my $payment = shift @payments;
911 $unapp_amount = $payment->unapplied;
912 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
913 $app->pkgnum( $payment->pkgnum )
914 if $conf->exists('pkg-balances') && $payment->pkgnum;
916 } elsif ( $app eq 'credit' ) {
918 my $credit = shift @credits;
919 $unapp_amount = $credit->credited;
920 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
921 $app->pkgnum( $credit->pkgnum )
922 if $conf->exists('pkg-balances') && $credit->pkgnum;
925 die "guru meditation #12 and 35";
929 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
930 warn "owed_pkgnum ". $app->pkgnum;
931 $owed = $self->owed_pkgnum($app->pkgnum);
935 next unless $owed > 0;
937 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
938 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
940 $app->invnum( $self->invnum );
942 my $error = $app->insert(%options);
944 $dbh->rollback if $oldAutoCommit;
945 return "Error inserting ". $app->table. " record: $error";
947 die $error if $error;
951 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
956 =item generate_email OPTION => VALUE ...
964 sender address, required
968 alternate template name, optional
972 text attachment arrayref, optional
976 email subject, optional
980 notice name instead of "Invoice", optional
984 Returns an argument list to be passed to L<FS::Misc::send_email>.
994 my $conf = $self->conf;
996 my $me = '[FS::cust_bill::generate_email]';
999 'from' => $args{'from'},
1000 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1004 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1005 'template' => $args{'template'},
1006 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1007 'no_coupon' => $args{'no_coupon'},
1010 my $cust_main = $self->cust_main;
1012 if (ref($args{'to'}) eq 'ARRAY') {
1013 $return{'to'} = $args{'to'};
1015 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1016 $cust_main->invoicing_list
1020 if ( $conf->exists('invoice_html') ) {
1022 warn "$me creating HTML/text multipart message"
1025 $return{'nobody'} = 1;
1027 my $alternative = build MIME::Entity
1028 'Type' => 'multipart/alternative',
1029 #'Encoding' => '7bit',
1030 'Disposition' => 'inline'
1034 if ( $conf->exists('invoice_email_pdf')
1035 and scalar($conf->config('invoice_email_pdf_note')) ) {
1037 warn "$me using 'invoice_email_pdf_note' in multipart message"
1039 $data = [ map { $_ . "\n" }
1040 $conf->config('invoice_email_pdf_note')
1045 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1047 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1048 $data = $args{'print_text'};
1050 $data = [ $self->print_text(\%opt) ];
1055 $alternative->attach(
1056 'Type' => 'text/plain',
1057 'Encoding' => 'quoted-printable',
1058 #'Encoding' => '7bit',
1060 'Disposition' => 'inline',
1067 if ( $conf->exists('invoice_email_pdf')
1068 and scalar($conf->config('invoice_email_pdf_note')) ) {
1070 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1074 $args{'from'} =~ /\@([\w\.\-]+)/;
1075 my $from = $1 || 'example.com';
1076 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1079 my $agentnum = $cust_main->agentnum;
1080 if ( defined($args{'template'}) && length($args{'template'})
1081 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1084 $logo = 'logo_'. $args{'template'}. '.png';
1088 my $image_data = $conf->config_binary( $logo, $agentnum);
1090 $image = build MIME::Entity
1091 'Type' => 'image/png',
1092 'Encoding' => 'base64',
1093 'Data' => $image_data,
1094 'Filename' => 'logo.png',
1095 'Content-ID' => "<$content_id>",
1098 if ($conf->exists('invoice-barcode')) {
1099 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1100 $barcode = build MIME::Entity
1101 'Type' => 'image/png',
1102 'Encoding' => 'base64',
1103 'Data' => $self->invoice_barcode(0),
1104 'Filename' => 'barcode.png',
1105 'Content-ID' => "<$barcode_content_id>",
1107 $opt{'barcode_cid'} = $barcode_content_id;
1110 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1113 $alternative->attach(
1114 'Type' => 'text/html',
1115 'Encoding' => 'quoted-printable',
1116 'Data' => [ '<html>',
1119 ' '. encode_entities($return{'subject'}),
1122 ' <body bgcolor="#e8e8e8">',
1127 'Disposition' => 'inline',
1128 #'Filename' => 'invoice.pdf',
1132 my @otherparts = ();
1133 if ( $cust_main->email_csv_cdr ) {
1135 push @otherparts, build MIME::Entity
1136 'Type' => 'text/csv',
1137 'Encoding' => '7bit',
1138 'Data' => [ map { "$_\n" }
1139 $self->call_details('prepend_billed_number' => 1)
1141 'Disposition' => 'attachment',
1142 'Filename' => 'usage-'. $self->invnum. '.csv',
1147 if ( $conf->exists('invoice_email_pdf') ) {
1152 # multipart/alternative
1158 my $related = build MIME::Entity 'Type' => 'multipart/related',
1159 'Encoding' => '7bit';
1161 #false laziness w/Misc::send_email
1162 $related->head->replace('Content-type',
1163 $related->mime_type.
1164 '; boundary="'. $related->head->multipart_boundary. '"'.
1165 '; type=multipart/alternative'
1168 $related->add_part($alternative);
1170 $related->add_part($image) if $image;
1172 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1174 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1178 #no other attachment:
1180 # multipart/alternative
1185 $return{'content-type'} = 'multipart/related';
1186 if ($conf->exists('invoice-barcode') && $barcode) {
1187 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1189 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1191 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1192 #$return{'disposition'} = 'inline';
1198 if ( $conf->exists('invoice_email_pdf') ) {
1199 warn "$me creating PDF attachment"
1202 #mime parts arguments a la MIME::Entity->build().
1203 $return{'mimeparts'} = [
1204 { $self->mimebuild_pdf(\%opt) }
1208 if ( $conf->exists('invoice_email_pdf')
1209 and scalar($conf->config('invoice_email_pdf_note')) ) {
1211 warn "$me using 'invoice_email_pdf_note'"
1213 $return{'body'} = [ map { $_ . "\n" }
1214 $conf->config('invoice_email_pdf_note')
1219 warn "$me not using 'invoice_email_pdf_note'"
1221 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1222 $return{'body'} = $args{'print_text'};
1224 $return{'body'} = [ $self->print_text(\%opt) ];
1237 Returns a list suitable for passing to MIME::Entity->build(), representing
1238 this invoice as PDF attachment.
1245 'Type' => 'application/pdf',
1246 'Encoding' => 'base64',
1247 'Data' => [ $self->print_pdf(@_) ],
1248 'Disposition' => 'attachment',
1249 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1253 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1255 Sends this invoice to the destinations configured for this customer: sends
1256 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1258 Options can be passed as a hashref (recommended) or as a list of up to
1259 four values for templatename, agentnum, invoice_from and amount.
1261 I<template>, if specified, is the name of a suffix for alternate invoices.
1263 I<agentnum>, if specified, means that this invoice will only be sent for customers
1264 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1265 single agent) or an arrayref of agentnums.
1267 I<invoice_from>, if specified, overrides the default email invoice From: address.
1269 I<amount>, if specified, only sends the invoice if the total amount owed on this
1270 invoice and all older invoices is greater than the specified amount.
1272 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1276 sub queueable_send {
1279 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1280 or die "invalid invoice number: " . $opt{invnum};
1282 my @args = ( $opt{template}, $opt{agentnum} );
1283 push @args, $opt{invoice_from}
1284 if exists($opt{invoice_from}) && $opt{invoice_from};
1286 my $error = $self->send( @args );
1287 die $error if $error;
1293 my $conf = $self->conf;
1295 my( $template, $invoice_from, $notice_name );
1297 my $balance_over = 0;
1301 $template = $opt->{'template'} || '';
1302 if ( $agentnums = $opt->{'agentnum'} ) {
1303 $agentnums = [ $agentnums ] unless ref($agentnums);
1305 $invoice_from = $opt->{'invoice_from'};
1306 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1307 $notice_name = $opt->{'notice_name'};
1309 $template = scalar(@_) ? shift : '';
1310 if ( scalar(@_) && $_[0] ) {
1311 $agentnums = ref($_[0]) ? shift : [ shift ];
1313 $invoice_from = shift if scalar(@_);
1314 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1317 return 'N/A' unless ! $agentnums
1318 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1321 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1323 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1324 $conf->config('invoice_from', $self->cust_main->agentnum );
1327 'template' => $template,
1328 'invoice_from' => $invoice_from,
1329 'notice_name' => ( $notice_name || 'Invoice' ),
1332 my @invoicing_list = $self->cust_main->invoicing_list;
1334 #$self->email_invoice(\%opt)
1336 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1338 #$self->print_invoice(\%opt)
1340 if grep { $_ eq 'POST' } @invoicing_list; #postal
1342 $self->fax_invoice(\%opt)
1343 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1349 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1351 Emails this invoice.
1353 Options can be passed as a hashref (recommended) or as a list of up to
1354 two values for templatename and invoice_from.
1356 I<template>, if specified, is the name of a suffix for alternate invoices.
1358 I<invoice_from>, if specified, overrides the default email invoice From: address.
1360 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1364 sub queueable_email {
1367 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1368 or die "invalid invoice number: " . $opt{invnum};
1370 my %args = ( 'template' => $opt{template} );
1371 $args{$_} = $opt{$_}
1372 foreach grep { exists($opt{$_}) && $opt{$_} }
1373 qw( invoice_from notice_name no_coupon );
1375 my $error = $self->email( \%args );
1376 die $error if $error;
1380 #sub email_invoice {
1383 return if $self->hide;
1384 my $conf = $self->conf;
1386 my( $template, $invoice_from, $notice_name, $no_coupon );
1389 $template = $opt->{'template'} || '';
1390 $invoice_from = $opt->{'invoice_from'};
1391 $notice_name = $opt->{'notice_name'} || 'Invoice';
1392 $no_coupon = $opt->{'no_coupon'} || 0;
1394 $template = scalar(@_) ? shift : '';
1395 $invoice_from = shift if scalar(@_);
1396 $notice_name = 'Invoice';
1400 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1401 $conf->config('invoice_from', $self->cust_main->agentnum );
1403 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1404 $self->cust_main->invoicing_list;
1406 if ( ! @invoicing_list ) { #no recipients
1407 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1408 die 'No recipients for customer #'. $self->custnum;
1410 #default: better to notify this person than silence
1411 @invoicing_list = ($invoice_from);
1415 my $subject = $self->email_subject($template);
1417 my $error = send_email(
1418 $self->generate_email(
1419 'from' => $invoice_from,
1420 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1421 'subject' => $subject,
1422 'template' => $template,
1423 'notice_name' => $notice_name,
1424 'no_coupon' => $no_coupon,
1427 die "can't email invoice: $error\n" if $error;
1428 #die "$error\n" if $error;
1434 my $conf = $self->conf;
1436 #my $template = scalar(@_) ? shift : '';
1439 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1442 my $cust_main = $self->cust_main;
1443 my $name = $cust_main->name;
1444 my $name_short = $cust_main->name_short;
1445 my $invoice_number = $self->invnum;
1446 my $invoice_date = $self->_date_pretty;
1448 eval qq("$subject");
1451 =item lpr_data HASHREF | [ TEMPLATE ]
1453 Returns the postscript or plaintext for this invoice as an arrayref.
1455 Options can be passed as a hashref (recommended) or as a single optional value
1458 I<template>, if specified, is the name of a suffix for alternate invoices.
1460 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1466 my $conf = $self->conf;
1467 my( $template, $notice_name );
1470 $template = $opt->{'template'} || '';
1471 $notice_name = $opt->{'notice_name'} || 'Invoice';
1473 $template = scalar(@_) ? shift : '';
1474 $notice_name = 'Invoice';
1478 'template' => $template,
1479 'notice_name' => $notice_name,
1482 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1483 [ $self->$method( \%opt ) ];
1486 =item print HASHREF | [ TEMPLATE ]
1488 Prints this invoice.
1490 Options can be passed as a hashref (recommended) or as a single optional
1493 I<template>, if specified, is the name of a suffix for alternate invoices.
1495 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1499 #sub print_invoice {
1502 return if $self->hide;
1503 my $conf = $self->conf;
1505 my( $template, $notice_name );
1508 $template = $opt->{'template'} || '';
1509 $notice_name = $opt->{'notice_name'} || 'Invoice';
1511 $template = scalar(@_) ? shift : '';
1512 $notice_name = 'Invoice';
1516 'template' => $template,
1517 'notice_name' => $notice_name,
1520 if($conf->exists('invoice_print_pdf')) {
1521 # Add the invoice to the current batch.
1522 $self->batch_invoice(\%opt);
1525 do_print $self->lpr_data(\%opt);
1529 =item fax_invoice HASHREF | [ TEMPLATE ]
1533 Options can be passed as a hashref (recommended) or as a single optional
1536 I<template>, if specified, is the name of a suffix for alternate invoices.
1538 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1544 return if $self->hide;
1545 my $conf = $self->conf;
1547 my( $template, $notice_name );
1550 $template = $opt->{'template'} || '';
1551 $notice_name = $opt->{'notice_name'} || 'Invoice';
1553 $template = scalar(@_) ? shift : '';
1554 $notice_name = 'Invoice';
1557 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1558 unless $conf->exists('invoice_latex');
1560 my $dialstring = $self->cust_main->getfield('fax');
1564 'template' => $template,
1565 'notice_name' => $notice_name,
1568 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1569 'dialstring' => $dialstring,
1571 die $error if $error;
1575 =item batch_invoice [ HASHREF ]
1577 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1578 isn't an open batch, one will be created.
1583 my ($self, $opt) = @_;
1584 my $bill_batch = $self->get_open_bill_batch;
1585 my $cust_bill_batch = FS::cust_bill_batch->new({
1586 batchnum => $bill_batch->batchnum,
1587 invnum => $self->invnum,
1589 return $cust_bill_batch->insert($opt);
1592 =item get_open_batch
1594 Returns the currently open batch as an FS::bill_batch object, creating a new
1595 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1600 sub get_open_bill_batch {
1602 my $conf = $self->conf;
1603 my $hashref = { status => 'O' };
1604 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1605 ? $self->cust_main->agentnum
1607 my $batch = qsearchs('bill_batch', $hashref);
1608 return $batch if $batch;
1609 $batch = FS::bill_batch->new($hashref);
1610 my $error = $batch->insert;
1611 die $error if $error;
1615 =item ftp_invoice [ TEMPLATENAME ]
1617 Sends this invoice data via FTP.
1619 TEMPLATENAME is unused?
1625 my $conf = $self->conf;
1626 my $template = scalar(@_) ? shift : '';
1629 'protocol' => 'ftp',
1630 'server' => $conf->config('cust_bill-ftpserver'),
1631 'username' => $conf->config('cust_bill-ftpusername'),
1632 'password' => $conf->config('cust_bill-ftppassword'),
1633 'dir' => $conf->config('cust_bill-ftpdir'),
1634 'format' => $conf->config('cust_bill-ftpformat'),
1638 =item spool_invoice [ TEMPLATENAME ]
1640 Spools this invoice data (see L<FS::spool_csv>)
1642 TEMPLATENAME is unused?
1648 my $conf = $self->conf;
1649 my $template = scalar(@_) ? shift : '';
1652 'format' => $conf->config('cust_bill-spoolformat'),
1653 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1657 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1659 Like B<send>, but only sends the invoice if it is the newest open invoice for
1664 sub send_if_newest {
1669 grep { $_->owed > 0 }
1670 qsearch('cust_bill', {
1671 'custnum' => $self->custnum,
1672 #'_date' => { op=>'>', value=>$self->_date },
1673 'invnum' => { op=>'>', value=>$self->invnum },
1680 =item send_csv OPTION => VALUE, ...
1682 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1686 protocol - currently only "ftp"
1692 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1693 and YYMMDDHHMMSS is a timestamp.
1695 See L</print_csv> for a description of the output format.
1700 my($self, %opt) = @_;
1704 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1705 mkdir $spooldir, 0700 unless -d $spooldir;
1707 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1708 my $file = "$spooldir/$tracctnum.csv";
1710 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1712 open(CSV, ">$file") or die "can't open $file: $!";
1720 if ( $opt{protocol} eq 'ftp' ) {
1721 eval "use Net::FTP;";
1723 $net = Net::FTP->new($opt{server}) or die @$;
1725 die "unknown protocol: $opt{protocol}";
1728 $net->login( $opt{username}, $opt{password} )
1729 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1731 $net->binary or die "can't set binary mode";
1733 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1735 $net->put($file) or die "can't put $file: $!";
1745 Spools CSV invoice data.
1751 =item format - 'default' or 'billco'
1753 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1755 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1757 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1764 my($self, %opt) = @_;
1766 my $cust_main = $self->cust_main;
1768 if ( $opt{'dest'} ) {
1769 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1770 $cust_main->invoicing_list;
1771 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1772 || ! keys %invoicing_list;
1775 if ( $opt{'balanceover'} ) {
1777 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1780 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1781 mkdir $spooldir, 0700 unless -d $spooldir;
1783 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1787 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1788 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1791 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1793 open(CSV, ">>$file") or die "can't open $file: $!";
1794 flock(CSV, LOCK_EX);
1799 if ( lc($opt{'format'}) eq 'billco' ) {
1801 flock(CSV, LOCK_UN);
1806 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1809 open(CSV,">>$file") or die "can't open $file: $!";
1810 flock(CSV, LOCK_EX);
1816 flock(CSV, LOCK_UN);
1823 =item print_csv OPTION => VALUE, ...
1825 Returns CSV data for this invoice.
1829 format - 'default' or 'billco'
1831 Returns a list consisting of two scalars. The first is a single line of CSV
1832 header information for this invoice. The second is one or more lines of CSV
1833 detail information for this invoice.
1835 If I<format> is not specified or "default", the fields of the CSV file are as
1838 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1842 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1844 B<record_type> is C<cust_bill> for the initial header line only. The
1845 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1846 fields are filled in.
1848 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1849 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1852 =item invnum - invoice number
1854 =item custnum - customer number
1856 =item _date - invoice date
1858 =item charged - total invoice amount
1860 =item first - customer first name
1862 =item last - customer first name
1864 =item company - company name
1866 =item address1 - address line 1
1868 =item address2 - address line 1
1878 =item pkg - line item description
1880 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1882 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1884 =item sdate - start date for recurring fee
1886 =item edate - end date for recurring fee
1890 If I<format> is "billco", the fields of the header CSV file are as follows:
1892 +-------------------------------------------------------------------+
1893 | FORMAT HEADER FILE |
1894 |-------------------------------------------------------------------|
1895 | Field | Description | Name | Type | Width |
1896 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1897 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1898 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1899 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1900 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1901 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1902 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1903 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1904 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1905 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1906 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1907 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1908 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1909 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1910 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1911 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1912 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1913 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1914 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1915 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1916 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1917 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1918 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1919 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1920 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1921 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1922 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1923 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1924 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1925 +-------+-------------------------------+------------+------+-------+
1927 If I<format> is "billco", the fields of the detail CSV file are as follows:
1929 FORMAT FOR DETAIL FILE
1931 Field | Description | Name | Type | Width
1932 1 | N/A-Leave Empty | RC | CHAR | 2
1933 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1934 3 | Account Number | TRACCTNUM | CHAR | 15
1935 4 | Invoice Number | TRINVOICE | CHAR | 15
1936 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1937 6 | Transaction Detail | DETAILS | CHAR | 100
1938 7 | Amount | AMT | NUM* | 9
1939 8 | Line Format Control** | LNCTRL | CHAR | 2
1940 9 | Grouping Code | GROUP | CHAR | 2
1941 10 | User Defined | ACCT CODE | CHAR | 15
1946 my($self, %opt) = @_;
1948 eval "use Text::CSV_XS";
1951 my $cust_main = $self->cust_main;
1953 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1955 if ( lc($opt{'format'}) eq 'billco' ) {
1958 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1960 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1962 my( $previous_balance, @unused ) = $self->previous; #previous balance
1964 my $pmt_cr_applied = 0;
1965 $pmt_cr_applied += $_->{'amount'}
1966 foreach ( $self->_items_payments, $self->_items_credits ) ;
1968 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1971 '', # 1 | N/A-Leave Empty CHAR 2
1972 '', # 2 | N/A-Leave Empty CHAR 15
1973 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1974 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1975 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1976 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1977 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1978 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1979 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1980 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1981 '', # 10 | Ancillary Billing Information CHAR 30
1982 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1983 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1986 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1989 $duedate, # 14 | Bill Due Date CHAR 10
1991 $previous_balance, # 15 | Previous Balance NUM* 9
1992 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1993 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1994 $totaldue, # 18 | Total Amt Due NUM* 9
1995 $totaldue, # 19 | Total Amt Due NUM* 9
1996 '', # 20 | 30 Day Aging NUM* 9
1997 '', # 21 | 60 Day Aging NUM* 9
1998 '', # 22 | 90 Day Aging NUM* 9
1999 'N', # 23 | Y/N CHAR 1
2000 '', # 24 | Remittance automation CHAR 100
2001 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2002 $self->custnum, # 26 | Customer Reference Number CHAR 15
2003 '0', # 27 | Federal Tax*** NUM* 9
2004 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2005 '0', # 29 | Other Taxes & Fees*** NUM* 9
2008 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2010 my ($previous_balance) = $self->previous;
2011 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2013 ($_->{pkgnum} || ''),
2016 } $self->_items_pkg;
2019 $cust_main->agentnum,
2023 $cust_main->address1,
2024 $cust_main->address2,
2030 time2str("%x", $self->_date),
2044 time2str("%x", $self->_date),
2045 sprintf("%.2f", $self->charged),
2046 ( map { $cust_main->getfield($_) }
2047 qw( first last company address1 address2 city state zip country ) ),
2049 ) or die "can't create csv";
2052 my $header = $csv->string. "\n";
2055 if ( lc($opt{'format'}) eq 'billco' ) {
2058 foreach my $item ( $self->_items_pkg ) {
2061 '', # 1 | N/A-Leave Empty CHAR 2
2062 '', # 2 | N/A-Leave Empty CHAR 15
2063 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2064 $self->invnum, # 4 | Invoice Number CHAR 15
2065 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2066 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2067 $item->{'amount'}, # 7 | Amount NUM* 9
2068 '', # 8 | Line Format Control** CHAR 2
2069 '', # 9 | Grouping Code CHAR 2
2070 '', # 10 | User Defined CHAR 15
2073 $detail .= $csv->string. "\n";
2077 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2083 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2085 my($pkg, $setup, $recur, $sdate, $edate);
2086 if ( $cust_bill_pkg->pkgnum ) {
2088 ($pkg, $setup, $recur, $sdate, $edate) = (
2089 $cust_bill_pkg->part_pkg->pkg,
2090 ( $cust_bill_pkg->setup != 0
2091 ? sprintf("%.2f", $cust_bill_pkg->setup )
2093 ( $cust_bill_pkg->recur != 0
2094 ? sprintf("%.2f", $cust_bill_pkg->recur )
2096 ( $cust_bill_pkg->sdate
2097 ? time2str("%x", $cust_bill_pkg->sdate)
2099 ($cust_bill_pkg->edate
2100 ?time2str("%x", $cust_bill_pkg->edate)
2104 } else { #pkgnum tax
2105 next unless $cust_bill_pkg->setup != 0;
2106 $pkg = $cust_bill_pkg->desc;
2107 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2108 ( $sdate, $edate ) = ( '', '' );
2114 ( map { '' } (1..11) ),
2115 ($pkg, $setup, $recur, $sdate, $edate)
2116 ) or die "can't create csv";
2118 $detail .= $csv->string. "\n";
2124 ( $header, $detail );
2130 Pays this invoice with a compliemntary payment. If there is an error,
2131 returns the error, otherwise returns false.
2137 my $cust_pay = new FS::cust_pay ( {
2138 'invnum' => $self->invnum,
2139 'paid' => $self->owed,
2142 'payinfo' => $self->cust_main->payinfo,
2150 Attempts to pay this invoice with a credit card payment via a
2151 Business::OnlinePayment realtime gateway. See
2152 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2153 for supported processors.
2159 $self->realtime_bop( 'CC', @_ );
2164 Attempts to pay this invoice with an electronic check (ACH) payment via a
2165 Business::OnlinePayment realtime gateway. See
2166 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2167 for supported processors.
2173 $self->realtime_bop( 'ECHECK', @_ );
2178 Attempts to pay this invoice with phone bill (LEC) payment via a
2179 Business::OnlinePayment realtime gateway. See
2180 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2181 for supported processors.
2187 $self->realtime_bop( 'LEC', @_ );
2191 my( $self, $method ) = (shift,shift);
2192 my $conf = $self->conf;
2195 my $cust_main = $self->cust_main;
2196 my $balance = $cust_main->balance;
2197 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2198 $amount = sprintf("%.2f", $amount);
2199 return "not run (balance $balance)" unless $amount > 0;
2201 my $description = 'Internet Services';
2202 if ( $conf->exists('business-onlinepayment-description') ) {
2203 my $dtempl = $conf->config('business-onlinepayment-description');
2205 my $agent_obj = $cust_main->agent
2206 or die "can't retreive agent for $cust_main (agentnum ".
2207 $cust_main->agentnum. ")";
2208 my $agent = $agent_obj->agent;
2209 my $pkgs = join(', ',
2210 map { $_->part_pkg->pkg }
2211 grep { $_->pkgnum } $self->cust_bill_pkg
2213 $description = eval qq("$dtempl");
2216 $cust_main->realtime_bop($method, $amount,
2217 'description' => $description,
2218 'invnum' => $self->invnum,
2219 #this didn't do what we want, it just calls apply_payments_and_credits
2221 'apply_to_invoice' => 1,
2224 #this changes application behavior: auto payments
2225 #triggered against a specific invoice are now applied
2226 #to that invoice instead of oldest open.
2232 =item batch_card OPTION => VALUE...
2234 Adds a payment for this invoice to the pending credit card batch (see
2235 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2236 runs the payment using a realtime gateway.
2241 my ($self, %options) = @_;
2242 my $cust_main = $self->cust_main;
2244 $options{invnum} = $self->invnum;
2246 $cust_main->batch_card(%options);
2249 sub _agent_template {
2251 $self->cust_main->agent_template;
2254 sub _agent_invoice_from {
2256 $self->cust_main->agent_invoice_from;
2259 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2261 Returns an text invoice, as a list of lines.
2263 Options can be passed as a hashref (recommended) or as a list of time, template
2264 and then any key/value pairs for any other options.
2266 I<time>, if specified, is used to control the printing of overdue messages. The
2267 default is now. It isn't the date of the invoice; that's the `_date' field.
2268 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2269 L<Time::Local> and L<Date::Parse> for conversion functions.
2271 I<template>, if specified, is the name of a suffix for alternate invoices.
2273 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2279 my( $today, $template, %opt );
2281 %opt = %{ shift() };
2282 $today = delete($opt{'time'}) || '';
2283 $template = delete($opt{template}) || '';
2285 ( $today, $template, %opt ) = @_;
2288 my %params = ( 'format' => 'template' );
2289 $params{'time'} = $today if $today;
2290 $params{'template'} = $template if $template;
2291 $params{$_} = $opt{$_}
2292 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2294 $self->print_generic( %params );
2297 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2299 Internal method - returns a filename of a filled-in LaTeX template for this
2300 invoice (Note: add ".tex" to get the actual filename), and a filename of
2301 an associated logo (with the .eps extension included).
2303 See print_ps and print_pdf for methods that return PostScript and PDF output.
2305 Options can be passed as a hashref (recommended) or as a list of time, template
2306 and then any key/value pairs for any other options.
2308 I<time>, if specified, is used to control the printing of overdue messages. The
2309 default is now. It isn't the date of the invoice; that's the `_date' field.
2310 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2311 L<Time::Local> and L<Date::Parse> for conversion functions.
2313 I<template>, if specified, is the name of a suffix for alternate invoices.
2315 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2321 my $conf = $self->conf;
2322 my( $today, $template, %opt );
2324 %opt = %{ shift() };
2325 $today = delete($opt{'time'}) || '';
2326 $template = delete($opt{template}) || '';
2328 ( $today, $template, %opt ) = @_;
2331 my %params = ( 'format' => 'latex' );
2332 $params{'time'} = $today if $today;
2333 $params{'template'} = $template if $template;
2334 $params{$_} = $opt{$_}
2335 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2337 $template ||= $self->_agent_template;
2339 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2340 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2344 ) or die "can't open temp file: $!\n";
2346 my $agentnum = $self->cust_main->agentnum;
2348 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2349 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2350 or die "can't write temp file: $!\n";
2352 print $lh $conf->config_binary('logo.eps', $agentnum)
2353 or die "can't write temp file: $!\n";
2356 $params{'logo_file'} = $lh->filename;
2358 if($conf->exists('invoice-barcode')){
2359 my $png_file = $self->invoice_barcode($dir);
2360 my $eps_file = $png_file;
2361 $eps_file =~ s/\.png$/.eps/g;
2362 $png_file =~ /(barcode.*png)/;
2364 $eps_file =~ /(barcode.*eps)/;
2367 my $curr_dir = cwd();
2369 # after painfuly long experimentation, it was determined that sam2p won't
2370 # accept : and other chars in the path, no matter how hard I tried to
2371 # escape them, hence the chdir (and chdir back, just to be safe)
2372 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2373 or die "sam2p failed: $!\n";
2377 $params{'barcode_file'} = $eps_file;
2380 my @filled_in = $self->print_generic( %params );
2382 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2386 ) or die "can't open temp file: $!\n";
2387 binmode($fh, ':utf8'); # language support
2388 print $fh join('', @filled_in );
2391 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2392 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2396 =item invoice_barcode DIR_OR_FALSE
2398 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2399 it is taken as the temp directory where the PNG file will be generated and the
2400 PNG file name is returned. Otherwise, the PNG image itself is returned.
2404 sub invoice_barcode {
2405 my ($self, $dir) = (shift,shift);
2407 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2408 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2409 my $gd = $gdbar->plot(Height => 30);
2412 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2416 ) or die "can't open temp file: $!\n";
2417 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2418 my $png_file = $bh->filename;
2425 =item print_generic OPTION => VALUE ...
2427 Internal method - returns a filled-in template for this invoice as a scalar.
2429 See print_ps and print_pdf for methods that return PostScript and PDF output.
2431 Non optional options include
2432 format - latex, html, template
2434 Optional options include
2436 template - a value used as a suffix for a configuration template
2438 time - a value used to control the printing of overdue messages. The
2439 default is now. It isn't the date of the invoice; that's the `_date' field.
2440 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2441 L<Time::Local> and L<Date::Parse> for conversion functions.
2445 unsquelch_cdr - overrides any per customer cdr squelching when true
2447 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2449 locale - override customer's locale
2453 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2454 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2455 # yes: fixed width/plain text printing will be borked
2457 my( $self, %params ) = @_;
2458 my $conf = $self->conf;
2459 my $today = $params{today} ? $params{today} : time;
2460 warn "$me print_generic called on $self with suffix $params{template}\n"
2463 my $format = $params{format};
2464 die "Unknown format: $format"
2465 unless $format =~ /^(latex|html|template)$/;
2467 my $cust_main = $self->cust_main;
2468 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2469 unless $cust_main->payname
2470 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2472 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2473 'html' => [ '<%=', '%>' ],
2474 'template' => [ '{', '}' ],
2477 warn "$me print_generic creating template\n"
2480 #create the template
2481 my $template = $params{template} ? $params{template} : $self->_agent_template;
2482 my $templatefile = "invoice_$format";
2483 $templatefile .= "_$template"
2484 if length($template) && $conf->exists($templatefile."_$template");
2485 my @invoice_template = map "$_\n", $conf->config($templatefile)
2486 or die "cannot load config data $templatefile";
2489 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2490 #change this to a die when the old code is removed
2491 warn "old-style invoice template $templatefile; ".
2492 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2493 $old_latex = 'true';
2494 @invoice_template = _translate_old_latex_format(@invoice_template);
2497 warn "$me print_generic creating T:T object\n"
2500 my $text_template = new Text::Template(
2502 SOURCE => \@invoice_template,
2503 DELIMITERS => $delimiters{$format},
2506 warn "$me print_generic compiling T:T object\n"
2509 $text_template->compile()
2510 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2513 # additional substitution could possibly cause breakage in existing templates
2514 my %convert_maps = (
2516 'notes' => sub { map "$_", @_ },
2517 'footer' => sub { map "$_", @_ },
2518 'smallfooter' => sub { map "$_", @_ },
2519 'returnaddress' => sub { map "$_", @_ },
2520 'coupon' => sub { map "$_", @_ },
2521 'summary' => sub { map "$_", @_ },
2527 s/%%(.*)$/<!-- $1 -->/g;
2528 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2529 s/\\begin\{enumerate\}/<ol>/g;
2531 s/\\end\{enumerate\}/<\/ol>/g;
2532 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2541 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2543 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2548 s/\\\\\*?\s*$/<BR>/;
2549 s/\\hyphenation\{[\w\s\-]+}//;
2554 'coupon' => sub { "" },
2555 'summary' => sub { "" },
2562 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2563 s/\\begin\{enumerate\}//g;
2565 s/\\end\{enumerate\}//g;
2566 s/\\textbf\{(.*)\}/$1/g;
2573 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2575 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2580 s/\\\\\*?\s*$/\n/; # dubious
2581 s/\\hyphenation\{[\w\s\-]+}//;
2585 'coupon' => sub { "" },
2586 'summary' => sub { "" },
2591 # hashes for differing output formats
2592 my %nbsps = ( 'latex' => '~',
2593 'html' => '', # '&nbps;' would be nice
2594 'template' => '', # not used
2596 my $nbsp = $nbsps{$format};
2598 my %escape_functions = ( 'latex' => \&_latex_escape,
2599 'html' => \&_html_escape_nbsp,#\&encode_entities,
2600 'template' => sub { shift },
2602 my $escape_function = $escape_functions{$format};
2603 my $escape_function_nonbsp = ($format eq 'html')
2604 ? \&_html_escape : $escape_function;
2606 my %date_formats = ( 'latex' => $date_format_long,
2607 'html' => $date_format_long,
2610 $date_formats{'html'} =~ s/ / /g;
2612 my $date_format = $date_formats{$format};
2614 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2616 'html' => sub { return '<b>'. shift(). '</b>'
2618 'template' => sub { shift },
2620 my $embolden_function = $embolden_functions{$format};
2622 my %newline_tokens = ( 'latex' => '\\\\',
2626 my $newline_token = $newline_tokens{$format};
2628 warn "$me generating template variables\n"
2631 # generate template variables
2634 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2638 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2644 $returnaddress = join("\n",
2645 $conf->config_orbase("invoice_${format}returnaddress", $template)
2648 } elsif ( grep /\S/,
2649 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2651 my $convert_map = $convert_maps{$format}{'returnaddress'};
2654 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2659 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2661 my $convert_map = $convert_maps{$format}{'returnaddress'};
2662 $returnaddress = join( "\n", &$convert_map(
2663 map { s/( {2,})/'~' x length($1)/eg;
2667 ( $conf->config('company_name', $self->cust_main->agentnum),
2668 $conf->config('company_address', $self->cust_main->agentnum),
2675 my $warning = "Couldn't find a return address; ".
2676 "do you need to set the company_address configuration value?";
2678 $returnaddress = $nbsp;
2679 #$returnaddress = $warning;
2683 warn "$me generating invoice data\n"
2686 my $agentnum = $self->cust_main->agentnum;
2688 my %invoice_data = (
2691 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2692 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2693 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2694 'returnaddress' => $returnaddress,
2695 'agent' => &$escape_function($cust_main->agent->agent),
2698 'invnum' => $self->invnum,
2699 'date' => time2str($date_format, $self->_date),
2700 'today' => time2str($date_format_long, $today),
2701 'terms' => $self->terms,
2702 'template' => $template, #params{'template'},
2703 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2704 'current_charges' => sprintf("%.2f", $self->charged),
2705 'duedate' => $self->due_date2str($rdate_format), #date_format?
2708 'custnum' => $cust_main->display_custnum,
2709 'agent_custid' => &$escape_function($cust_main->agent_custid),
2710 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2711 payname company address1 address2 city state zip fax
2715 'ship_enable' => $conf->exists('invoice-ship_address'),
2716 'unitprices' => $conf->exists('invoice-unitprice'),
2717 'smallernotes' => $conf->exists('invoice-smallernotes'),
2718 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2719 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2721 #layout info -- would be fancy to calc some of this and bury the template
2723 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2724 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2725 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2726 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2727 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2728 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2729 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2730 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2731 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2732 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2734 # better hang on to conf_dir for a while (for old templates)
2735 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2737 #these are only used when doing paged plaintext
2744 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2745 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2746 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2747 # eval to avoid death for unimplemented languages
2748 my $dh = eval { Date::Language->new($info{'name'}) } ||
2749 Date::Language->new(); # fall back to English
2750 # prototype here to silence warnings
2751 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2752 # eventually use this date handle everywhere in here, too
2754 my $min_sdate = 999999999999;
2756 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2757 next unless $cust_bill_pkg->pkgnum > 0;
2758 $min_sdate = $cust_bill_pkg->sdate
2759 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2760 $max_edate = $cust_bill_pkg->edate
2761 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2764 $invoice_data{'bill_period'} = '';
2765 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2766 . " to " . time2str('%e %h', $max_edate)
2767 if ($max_edate != 0 && $min_sdate != 999999999999);
2769 $invoice_data{finance_section} = '';
2770 if ( $conf->config('finance_pkgclass') ) {
2772 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2773 $invoice_data{finance_section} = $pkg_class->categoryname;
2775 $invoice_data{finance_amount} = '0.00';
2776 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2778 my $countrydefault = $conf->config('countrydefault') || 'US';
2779 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2780 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2781 my $method = $prefix.$_;
2782 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2784 $invoice_data{'ship_country'} = ''
2785 if ( $invoice_data{'ship_country'} eq $countrydefault );
2787 $invoice_data{'cid'} = $params{'cid'}
2790 if ( $cust_main->country eq $countrydefault ) {
2791 $invoice_data{'country'} = '';
2793 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2797 $invoice_data{'address'} = \@address;
2799 $cust_main->payname.
2800 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2801 ? " (P.O. #". $cust_main->payinfo. ")"
2805 push @address, $cust_main->company
2806 if $cust_main->company;
2807 push @address, $cust_main->address1;
2808 push @address, $cust_main->address2
2809 if $cust_main->address2;
2811 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2812 push @address, $invoice_data{'country'}
2813 if $invoice_data{'country'};
2815 while (scalar(@address) < 5);
2817 $invoice_data{'logo_file'} = $params{'logo_file'}
2818 if $params{'logo_file'};
2819 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2820 if $params{'barcode_file'};
2821 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2822 if $params{'barcode_img'};
2823 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2824 if $params{'barcode_cid'};
2826 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2827 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2828 #my $balance_due = $self->owed + $pr_total - $cr_total;
2829 my $balance_due = $self->owed + $pr_total;
2831 # the customer's current balance as shown on the invoice before this one
2832 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2834 # the change in balance from that invoice to this one
2835 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2837 # the sum of amount owed on all previous invoices
2838 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2840 # the sum of amount owed on all invoices
2841 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2843 # info from customer's last invoice before this one, for some
2845 $invoice_data{'last_bill'} = {};
2846 my $last_bill = $pr_cust_bill[-1];
2848 $invoice_data{'last_bill'} = {
2849 '_date' => $last_bill->_date, #unformatted
2850 # all we need for now
2854 my $summarypage = '';
2855 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2858 $invoice_data{'summarypage'} = $summarypage;
2860 warn "$me substituting variables in notes, footer, smallfooter\n"
2863 my @include = (qw( notes footer smallfooter ));
2864 push @include, 'coupon' unless $params{'no_coupon'};
2865 foreach my $include (@include) {
2867 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2870 if ( $conf->exists($inc_file, $agentnum)
2871 && length( $conf->config($inc_file, $agentnum) ) ) {
2873 @inc_src = $conf->config($inc_file, $agentnum);
2877 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2879 my $convert_map = $convert_maps{$format}{$include};
2881 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2882 s/--\@\]/$delimiters{$format}[1]/g;
2885 &$convert_map( $conf->config($inc_file, $agentnum) );
2889 my $inc_tt = new Text::Template (
2891 SOURCE => [ map "$_\n", @inc_src ],
2892 DELIMITERS => $delimiters{$format},
2893 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2895 unless ( $inc_tt->compile() ) {
2896 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2897 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2901 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2903 $invoice_data{$include} =~ s/\n+$//
2904 if ($format eq 'latex');
2907 # let invoices use either of these as needed
2908 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2909 ? $cust_main->payinfo : '';
2910 $invoice_data{'po_line'} =
2911 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2912 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2915 my %money_chars = ( 'latex' => '',
2916 'html' => $conf->config('money_char') || '$',
2919 my $money_char = $money_chars{$format};
2921 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2922 'html' => $conf->config('money_char') || '$',
2925 my $other_money_char = $other_money_chars{$format};
2926 $invoice_data{'dollar'} = $other_money_char;
2928 my @detail_items = ();
2929 my @total_items = ();
2933 $invoice_data{'detail_items'} = \@detail_items;
2934 $invoice_data{'total_items'} = \@total_items;
2935 $invoice_data{'buf'} = \@buf;
2936 $invoice_data{'sections'} = \@sections;
2938 warn "$me generating sections\n"
2941 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2942 'subtotal' => $other_money_char.
2943 sprintf('%.2f', $pr_total),
2944 'summarized' => '', #why? $summarypage ? 'Y' : '',
2946 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2947 join(' / ', map { $cust_main->balance_date_range(@$_) }
2948 $self->_prior_month30s
2950 if $conf->exists('invoice_include_aging');
2953 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2954 'subtotal' => $taxtotal, # adjusted below
2956 my $tax_weight = _pkg_category($tax_section->{description})
2957 ? _pkg_category($tax_section->{description})->weight
2959 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2960 $tax_section->{'sort_weight'} = $tax_weight;
2963 my $adjusttotal = 0;
2964 my $adjust_section = { 'description' =>
2965 $self->mt('Credits, Payments, and Adjustments'),
2966 'subtotal' => 0, # adjusted below
2968 my $adjust_weight = _pkg_category($adjust_section->{description})
2969 ? _pkg_category($adjust_section->{description})->weight
2971 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2972 $adjust_section->{'sort_weight'} = $adjust_weight;
2974 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2975 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2976 $invoice_data{'multisection'} = $multisection;
2977 my $late_sections = [];
2978 my $extra_sections = [];
2979 my $extra_lines = ();
2980 if ( $multisection ) {
2981 ($extra_sections, $extra_lines) =
2982 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2983 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2985 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2987 push @detail_items, @$extra_lines if $extra_lines;
2989 $self->_items_sections( $late_sections, # this could stand a refactor
2991 $escape_function_nonbsp,
2995 if ($conf->exists('svc_phone_sections')) {
2996 my ($phone_sections, $phone_lines) =
2997 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2998 push @{$late_sections}, @$phone_sections;
2999 push @detail_items, @$phone_lines;
3001 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3002 my ($accountcode_section, $accountcode_lines) =
3003 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3004 if ( scalar(@$accountcode_lines) ) {
3005 push @{$late_sections}, $accountcode_section;
3006 push @detail_items, @$accountcode_lines;
3009 } else {# not multisection
3010 # make a default section
3011 push @sections, { 'description' => '', 'subtotal' => '',
3012 'no_subtotal' => 1 };
3013 # and calculate the finance charge total, since it won't get done otherwise.
3014 # XXX possibly other totals?
3015 # XXX possibly finance_pkgclass should not be used in this manner?
3016 if ( $conf->exists('finance_pkgclass') ) {
3017 my @finance_charges;
3018 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3019 if ( grep { $_->section eq $invoice_data{finance_section} }
3020 $cust_bill_pkg->cust_bill_pkg_display ) {
3021 # I think these are always setup fees, but just to be sure...
3022 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3025 $invoice_data{finance_amount} =
3026 sprintf('%.2f', sum( @finance_charges ) || 0);
3030 unless ( $conf->exists('disable_previous_balance', $agentnum)
3031 || $conf->exists('previous_balance-summary_only')
3035 warn "$me adding previous balances\n"
3038 foreach my $line_item ( $self->_items_previous ) {
3041 ext_description => [],
3043 $detail->{'ref'} = $line_item->{'pkgnum'};
3044 $detail->{'quantity'} = 1;
3045 $detail->{'section'} = $previous_section;
3046 $detail->{'description'} = &$escape_function($line_item->{'description'});
3047 if ( exists $line_item->{'ext_description'} ) {
3048 @{$detail->{'ext_description'}} = map {
3049 &$escape_function($_);
3050 } @{$line_item->{'ext_description'}};
3052 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3053 $line_item->{'amount'};
3054 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3056 push @detail_items, $detail;
3057 push @buf, [ $detail->{'description'},
3058 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3064 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3066 push @buf, ['','-----------'];
3067 push @buf, [ $self->mt('Total Previous Balance'),
3068 $money_char. sprintf("%10.2f", $pr_total) ];
3072 if ( $conf->exists('svc_phone-did-summary') ) {
3073 warn "$me adding DID summary\n"
3076 my ($didsummary,$minutes) = $self->_did_summary;
3077 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3079 { 'description' => $didsummary_desc,
3080 'ext_description' => [ $didsummary, $minutes ],
3084 foreach my $section (@sections, @$late_sections) {
3086 warn "$me adding section \n". Dumper($section)
3089 # begin some normalization
3090 $section->{'subtotal'} = $section->{'amount'}
3092 && !exists($section->{subtotal})
3093 && exists($section->{amount});
3095 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3096 if ( $invoice_data{finance_section} &&
3097 $section->{'description'} eq $invoice_data{finance_section} );
3099 $section->{'subtotal'} = $other_money_char.
3100 sprintf('%.2f', $section->{'subtotal'})
3103 # continue some normalization
3104 $section->{'amount'} = $section->{'subtotal'}
3108 if ( $section->{'description'} ) {
3109 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3114 warn "$me setting options\n"
3117 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3119 $options{'section'} = $section if $multisection;
3120 $options{'format'} = $format;
3121 $options{'escape_function'} = $escape_function;
3122 $options{'no_usage'} = 1 unless $unsquelched;
3123 $options{'unsquelched'} = $unsquelched;
3124 $options{'summary_page'} = $summarypage;
3125 $options{'skip_usage'} =
3126 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3127 $options{'multilocation'} = $multilocation;
3128 $options{'multisection'} = $multisection;
3130 warn "$me searching for line items\n"
3133 foreach my $line_item ( $self->_items_pkg(%options) ) {
3135 warn "$me adding line item $line_item\n"
3139 ext_description => [],
3141 $detail->{'ref'} = $line_item->{'pkgnum'};
3142 $detail->{'quantity'} = $line_item->{'quantity'};
3143 $detail->{'section'} = $section;
3144 $detail->{'description'} = &$escape_function($line_item->{'description'});
3145 if ( exists $line_item->{'ext_description'} ) {
3146 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3148 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3149 $line_item->{'amount'};
3150 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3151 $line_item->{'unit_amount'};
3152 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3154 $detail->{'sdate'} = $line_item->{'sdate'};
3155 $detail->{'edate'} = $line_item->{'edate'};
3156 $detail->{'seconds'} = $line_item->{'seconds'};
3158 push @detail_items, $detail;
3159 push @buf, ( [ $detail->{'description'},
3160 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3162 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3166 if ( $section->{'description'} ) {
3167 push @buf, ( ['','-----------'],
3168 [ $section->{'description'}. ' sub-total',
3169 $section->{'subtotal'} # already formatted this
3178 $invoice_data{current_less_finance} =
3179 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3181 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3182 || $conf->exists('previous_balance-summary_only') )
3184 unshift @sections, $previous_section if $pr_total;
3187 warn "$me adding taxes\n"
3190 foreach my $tax ( $self->_items_tax ) {
3192 $taxtotal += $tax->{'amount'};
3194 my $description = &$escape_function( $tax->{'description'} );
3195 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3197 if ( $multisection ) {
3199 my $money = $old_latex ? '' : $money_char;
3200 push @detail_items, {
3201 ext_description => [],
3204 description => $description,
3205 amount => $money. $amount,
3207 section => $tax_section,
3212 push @total_items, {
3213 'total_item' => $description,
3214 'total_amount' => $other_money_char. $amount,
3219 push @buf,[ $description,
3220 $money_char. $amount,
3227 $total->{'total_item'} = $self->mt('Sub-total');
3228 $total->{'total_amount'} =
3229 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3231 if ( $multisection ) {
3232 $tax_section->{'subtotal'} = $other_money_char.
3233 sprintf('%.2f', $taxtotal);
3234 $tax_section->{'pretotal'} = 'New charges sub-total '.
3235 $total->{'total_amount'};
3236 push @sections, $tax_section if $taxtotal;
3238 unshift @total_items, $total;
3241 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3243 push @buf,['','-----------'];
3244 push @buf,[$self->mt(
3245 $conf->exists('disable_previous_balance', $agentnum)
3247 : 'Total New Charges'
3249 $money_char. sprintf("%10.2f",$self->charged) ];
3255 $item = $conf->config('previous_balance-exclude_from_total')
3256 || 'Total New Charges'
3257 if $conf->exists('previous_balance-exclude_from_total');
3258 my $amount = $self->charged +
3259 ( $conf->exists('disable_previous_balance', $agentnum) ||
3260 $conf->exists('previous_balance-exclude_from_total')
3264 $total->{'total_item'} = &$embolden_function($self->mt($item));
3265 $total->{'total_amount'} =
3266 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3267 if ( $multisection ) {
3268 if ( $adjust_section->{'sort_weight'} ) {
3269 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3270 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3272 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3273 $other_money_char. sprintf('%.2f', $self->charged );
3276 push @total_items, $total;
3278 push @buf,['','-----------'];
3281 sprintf( '%10.2f', $amount )
3286 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3287 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3290 my $credittotal = 0;
3291 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3294 $total->{'total_item'} = &$escape_function($credit->{'description'});
3295 $credittotal += $credit->{'amount'};
3296 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3297 $adjusttotal += $credit->{'amount'};
3298 if ( $multisection ) {
3299 my $money = $old_latex ? '' : $money_char;
3300 push @detail_items, {
3301 ext_description => [],
3304 description => &$escape_function($credit->{'description'}),
3305 amount => $money. $credit->{'amount'},
3307 section => $adjust_section,
3310 push @total_items, $total;
3314 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3317 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3318 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3322 my $paymenttotal = 0;
3323 foreach my $payment ( $self->_items_payments ) {
3325 $total->{'total_item'} = &$escape_function($payment->{'description'});
3326 $paymenttotal += $payment->{'amount'};
3327 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3328 $adjusttotal += $payment->{'amount'};
3329 if ( $multisection ) {
3330 my $money = $old_latex ? '' : $money_char;
3331 push @detail_items, {
3332 ext_description => [],
3335 description => &$escape_function($payment->{'description'}),
3336 amount => $money. $payment->{'amount'},
3338 section => $adjust_section,
3341 push @total_items, $total;
3343 push @buf, [ $payment->{'description'},
3344 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3347 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3349 if ( $multisection ) {
3350 $adjust_section->{'subtotal'} = $other_money_char.
3351 sprintf('%.2f', $adjusttotal);
3352 push @sections, $adjust_section
3353 unless $adjust_section->{sort_weight};
3356 # create Balance Due message
3359 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3360 $total->{'total_amount'} =
3361 &$embolden_function(
3362 $other_money_char. sprintf('%.2f', $summarypage
3364 $self->billing_balance
3365 : $self->owed + $pr_total
3368 if ( $multisection && !$adjust_section->{sort_weight} ) {
3369 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3370 $total->{'total_amount'};
3372 push @total_items, $total;
3374 push @buf,['','-----------'];
3375 push @buf,[$self->balance_due_msg, $money_char.
3376 sprintf("%10.2f", $balance_due ) ];
3379 if ( $conf->exists('previous_balance-show_credit')
3380 and $cust_main->balance < 0 ) {
3381 my $credit_total = {
3382 'total_item' => &$embolden_function($self->credit_balance_msg),
3383 'total_amount' => &$embolden_function(
3384 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3387 if ( $multisection ) {
3388 $adjust_section->{'posttotal'} .= $newline_token .
3389 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3392 push @total_items, $credit_total;
3394 push @buf,['','-----------'];
3395 push @buf,[$self->credit_balance_msg, $money_char.
3396 sprintf("%10.2f", -$cust_main->balance ) ];
3400 if ( $multisection ) {
3401 if ($conf->exists('svc_phone_sections')) {
3403 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3404 $total->{'total_amount'} =
3405 &$embolden_function(
3406 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3408 my $last_section = pop @sections;
3409 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3410 $total->{'total_amount'};
3411 push @sections, $last_section;
3413 push @sections, @$late_sections
3417 # make a discounts-available section, even without multisection
3418 if ( $conf->exists('discount-show_available')
3419 and my @discounts_avail = $self->_items_discounts_avail ) {
3420 my $discount_section = {
3421 'description' => $self->mt('Discounts Available'),
3426 push @sections, $discount_section;
3427 push @detail_items, map { +{
3428 'ref' => '', #should this be something else?
3429 'section' => $discount_section,
3430 'description' => &$escape_function( $_->{description} ),
3431 'amount' => $money_char . &$escape_function( $_->{amount} ),
3432 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3433 } } @discounts_avail;
3436 # All sections and items are built; now fill in templates.
3437 my @includelist = ();
3438 push @includelist, 'summary' if $summarypage;
3439 foreach my $include ( @includelist ) {
3441 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3444 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3446 @inc_src = $conf->config($inc_file, $agentnum);
3450 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3452 my $convert_map = $convert_maps{$format}{$include};
3454 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3455 s/--\@\]/$delimiters{$format}[1]/g;
3458 &$convert_map( $conf->config($inc_file, $agentnum) );
3462 my $inc_tt = new Text::Template (
3464 SOURCE => [ map "$_\n", @inc_src ],
3465 DELIMITERS => $delimiters{$format},
3466 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3468 unless ( $inc_tt->compile() ) {
3469 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3470 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3474 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3476 $invoice_data{$include} =~ s/\n+$//
3477 if ($format eq 'latex');
3482 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3483 /invoice_lines\((\d*)\)/;
3484 $invoice_lines += $1 || scalar(@buf);
3487 die "no invoice_lines() functions in template?"
3488 if ( $format eq 'template' && !$wasfunc );
3490 if ($format eq 'template') {
3492 if ( $invoice_lines ) {
3493 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3494 $invoice_data{'total_pages'}++
3495 if scalar(@buf) % $invoice_lines;
3498 #setup subroutine for the template
3499 $invoice_data{invoice_lines} = sub {
3500 my $lines = shift || scalar(@buf);
3512 push @collect, split("\n",
3513 $text_template->fill_in( HASH => \%invoice_data )
3515 $invoice_data{'page'}++;
3517 map "$_\n", @collect;
3519 # this is where we actually create the invoice
3520 warn "filling in template for invoice ". $self->invnum. "\n"
3522 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3525 $text_template->fill_in(HASH => \%invoice_data);
3529 # helper routine for generating date ranges
3530 sub _prior_month30s {
3533 [ 1, 2592000 ], # 0-30 days ago
3534 [ 2592000, 5184000 ], # 30-60 days ago
3535 [ 5184000, 7776000 ], # 60-90 days ago
3536 [ 7776000, 0 ], # 90+ days ago
3539 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3540 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3545 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3547 Returns an postscript invoice, as a scalar.
3549 Options can be passed as a hashref (recommended) or as a list of time, template
3550 and then any key/value pairs for any other options.
3552 I<time> an optional value used to control the printing of overdue messages. The
3553 default is now. It isn't the date of the invoice; that's the `_date' field.
3554 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3555 L<Time::Local> and L<Date::Parse> for conversion functions.
3557 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3564 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3565 my $ps = generate_ps($file);
3567 unlink($barcodefile) if $barcodefile;
3572 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3574 Returns an PDF invoice, as a scalar.
3576 Options can be passed as a hashref (recommended) or as a list of time, template
3577 and then any key/value pairs for any other options.
3579 I<time> an optional value used to control the printing of overdue messages. The
3580 default is now. It isn't the date of the invoice; that's the `_date' field.
3581 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3582 L<Time::Local> and L<Date::Parse> for conversion functions.
3584 I<template>, if specified, is the name of a suffix for alternate invoices.
3586 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3593 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3594 my $pdf = generate_pdf($file);
3596 unlink($barcodefile) if $barcodefile;
3601 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3603 Returns an HTML invoice, as a scalar.
3605 I<time> an optional value used to control the printing of overdue messages. The
3606 default is now. It isn't the date of the invoice; that's the `_date' field.
3607 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3608 L<Time::Local> and L<Date::Parse> for conversion functions.
3610 I<template>, if specified, is the name of a suffix for alternate invoices.
3612 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3614 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3615 when emailing the invoice as part of a multipart/related MIME email.
3623 %params = %{ shift() };
3625 $params{'time'} = shift;
3626 $params{'template'} = shift;
3627 $params{'cid'} = shift;
3630 $params{'format'} = 'html';
3632 $self->print_generic( %params );
3635 # quick subroutine for print_latex
3637 # There are ten characters that LaTeX treats as special characters, which
3638 # means that they do not simply typeset themselves:
3639 # # $ % & ~ _ ^ \ { }
3641 # TeX ignores blanks following an escaped character; if you want a blank (as
3642 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3646 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3647 $value =~ s/([<>])/\$$1\$/g;
3653 encode_entities($value);
3657 sub _html_escape_nbsp {
3658 my $value = _html_escape(shift);
3659 $value =~ s/ +/ /g;
3663 #utility methods for print_*
3665 sub _translate_old_latex_format {
3666 warn "_translate_old_latex_format called\n"
3673 if ( $line =~ /^%%Detail\s*$/ ) {
3675 push @template, q![@--!,
3676 q! foreach my $_tr_line (@detail_items) {!,
3677 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3678 q! $_tr_line->{'description'} .= !,
3679 q! "\\tabularnewline\n~~".!,
3680 q! join( "\\tabularnewline\n~~",!,
3681 q! @{$_tr_line->{'ext_description'}}!,
3685 while ( ( my $line_item_line = shift )
3686 !~ /^%%EndDetail\s*$/ ) {
3687 $line_item_line =~ s/'/\\'/g; # nice LTS
3688 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3689 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3690 push @template, " \$OUT .= '$line_item_line';";
3693 push @template, '}',
3696 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3698 push @template, '[@--',
3699 ' foreach my $_tr_line (@total_items) {';
3701 while ( ( my $total_item_line = shift )
3702 !~ /^%%EndTotalDetails\s*$/ ) {
3703 $total_item_line =~ s/'/\\'/g; # nice LTS
3704 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3705 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3706 push @template, " \$OUT .= '$total_item_line';";
3709 push @template, '}',
3713 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3714 push @template, $line;
3720 warn "$_\n" foreach @template;
3728 my $conf = $self->conf;
3730 #check for an invoice-specific override
3731 return $self->invoice_terms if $self->invoice_terms;
3733 #check for a customer- specific override
3734 my $cust_main = $self->cust_main;
3735 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3737 #use configured default
3738 $conf->config('invoice_default_terms') || '';
3744 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3745 $duedate = $self->_date() + ( $1 * 86400 );
3752 $self->due_date ? time2str(shift, $self->due_date) : '';
3755 sub balance_due_msg {
3757 my $msg = $self->mt('Balance Due');
3758 return $msg unless $self->terms;
3759 if ( $self->due_date ) {
3760 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3761 $self->due_date2str($date_format);
3762 } elsif ( $self->terms ) {
3763 $msg .= ' - '. $self->terms;
3768 sub balance_due_date {
3770 my $conf = $self->conf;
3772 if ( $conf->exists('invoice_default_terms')
3773 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3774 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3779 sub credit_balance_msg {
3781 $self->mt('Credit Balance Remaining')
3784 =item invnum_date_pretty
3786 Returns a string with the invoice number and date, for example:
3787 "Invoice #54 (3/20/2008)"
3791 sub invnum_date_pretty {
3793 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3798 Returns a string with the date, for example: "3/20/2008"
3804 time2str($date_format, $self->_date);
3807 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3809 Generate section information for all items appearing on this invoice.
3810 This will only be called for multi-section invoices.
3812 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3813 related display records (L<FS::cust_bill_pkg_display>) and organize
3814 them into two groups ("early" and "late" according to whether they come
3815 before or after the total), then into sections. A subtotal is calculated
3818 Section descriptions are returned in sort weight order. Each consists
3819 of a hash containing:
3821 description: the package category name, escaped
3822 subtotal: the total charges in that section
3823 tax_section: a flag indicating that the section contains only tax charges
3824 summarized: same as tax_section, for some reason
3825 sort_weight: the package category's sort weight
3827 If 'condense' is set on the display record, it also contains everything
3828 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3829 coderefs to generate parts of the invoice. This is not advised.
3833 LATE: an arrayref to push the "late" section hashes onto. The "early"
3834 group is simply returned from the method.
3836 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3837 Turning this on has the following effects:
3838 - Ignores display items with the 'summary' flag.
3839 - Combines all items into the "early" group.
3840 - Creates sections for all non-disabled package categories, even if they
3841 have no charges on this invoice, as well as a section with no name.
3843 ESCAPE: an escape function to use for section titles.
3845 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3846 sorted list. If there are any of these, section subtotals exclude
3849 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3850 passed through to C<_condense_section()>.
3854 use vars qw(%pkg_category_cache);
3855 sub _items_sections {
3858 my $summarypage = shift;
3860 my $extra_sections = shift;
3864 my %late_subtotal = ();
3867 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3870 my $usage = $cust_bill_pkg->usage;
3872 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3873 next if ( $display->summary && $summarypage );
3875 my $section = $display->section;
3876 my $type = $display->type;
3878 $not_tax{$section} = 1
3879 unless $cust_bill_pkg->pkgnum == 0;
3881 if ( $display->post_total && !$summarypage ) {
3882 if (! $type || $type eq 'S') {
3883 $late_subtotal{$section} += $cust_bill_pkg->setup
3884 if $cust_bill_pkg->setup != 0;
3888 $late_subtotal{$section} += $cust_bill_pkg->recur
3889 if $cust_bill_pkg->recur != 0;
3892 if ($type && $type eq 'R') {
3893 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3894 if $cust_bill_pkg->recur != 0;
3897 if ($type && $type eq 'U') {
3898 $late_subtotal{$section} += $usage
3899 unless scalar(@$extra_sections);
3904 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3906 if (! $type || $type eq 'S') {
3907 $subtotal{$section} += $cust_bill_pkg->setup
3908 if $cust_bill_pkg->setup != 0;
3912 $subtotal{$section} += $cust_bill_pkg->recur
3913 if $cust_bill_pkg->recur != 0;
3916 if ($type && $type eq 'R') {
3917 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3918 if $cust_bill_pkg->recur != 0;
3921 if ($type && $type eq 'U') {
3922 $subtotal{$section} += $usage
3923 unless scalar(@$extra_sections);
3932 %pkg_category_cache = ();
3934 push @$late, map { { 'description' => &{$escape}($_),
3935 'subtotal' => $late_subtotal{$_},
3937 'sort_weight' => ( _pkg_category($_)
3938 ? _pkg_category($_)->weight
3941 ((_pkg_category($_) && _pkg_category($_)->condense)
3942 ? $self->_condense_section($format)
3946 sort _sectionsort keys %late_subtotal;
3949 if ( $summarypage ) {
3950 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3951 map { $_->categoryname } qsearch('pkg_category', {});
3952 push @sections, '' if exists($subtotal{''});
3954 @sections = keys %subtotal;
3957 my @early = map { { 'description' => &{$escape}($_),
3958 'subtotal' => $subtotal{$_},
3959 'summarized' => $not_tax{$_} ? '' : 'Y',
3960 'tax_section' => $not_tax{$_} ? '' : 'Y',
3961 'sort_weight' => ( _pkg_category($_)
3962 ? _pkg_category($_)->weight
3965 ((_pkg_category($_) && _pkg_category($_)->condense)
3966 ? $self->_condense_section($format)
3971 push @early, @$extra_sections if $extra_sections;
3973 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3977 #helper subs for above
3980 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3984 my $categoryname = shift;
3985 $pkg_category_cache{$categoryname} ||=
3986 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3989 my %condensed_format = (
3990 'label' => [ qw( Description Qty Amount ) ],
3992 sub { shift->{description} },
3993 sub { shift->{quantity} },
3994 sub { my($href, %opt) = @_;
3995 ($opt{dollar} || ''). $href->{amount};
3998 'align' => [ qw( l r r ) ],
3999 'span' => [ qw( 5 1 1 ) ], # unitprices?
4000 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4003 sub _condense_section {
4004 my ( $self, $format ) = ( shift, shift );
4006 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4007 qw( description_generator
4010 total_line_generator
4015 sub _condensed_generator_defaults {
4016 my ( $self, $format ) = ( shift, shift );
4017 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4026 sub _condensed_header_generator {
4027 my ( $self, $format ) = ( shift, shift );
4029 my ( $f, $prefix, $suffix, $separator, $column ) =
4030 _condensed_generator_defaults($format);
4032 if ($format eq 'latex') {
4033 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4034 $suffix = "\\\\\n\\hline";
4037 sub { my ($d,$a,$s,$w) = @_;
4038 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4040 } elsif ( $format eq 'html' ) {
4041 $prefix = '<th></th>';
4045 sub { my ($d,$a,$s,$w) = @_;
4046 return qq!<th align="$html_align{$a}">$d</th>!;
4054 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4056 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4059 $prefix. join($separator, @result). $suffix;
4064 sub _condensed_description_generator {
4065 my ( $self, $format ) = ( shift, shift );
4067 my ( $f, $prefix, $suffix, $separator, $column ) =
4068 _condensed_generator_defaults($format);
4070 my $money_char = '$';
4071 if ($format eq 'latex') {
4072 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4074 $separator = " & \n";
4076 sub { my ($d,$a,$s,$w) = @_;
4077 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4079 $money_char = '\\dollar';
4080 }elsif ( $format eq 'html' ) {
4081 $prefix = '"><td align="center"></td>';
4085 sub { my ($d,$a,$s,$w) = @_;
4086 return qq!<td align="$html_align{$a}">$d</td>!;
4088 #$money_char = $conf->config('money_char') || '$';
4089 $money_char = ''; # this is madness
4097 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4099 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4101 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4102 map { $f->{$_}->[$i] } qw(align span width)
4106 $prefix. join( $separator, @result ). $suffix;
4111 sub _condensed_total_generator {
4112 my ( $self, $format ) = ( shift, shift );
4114 my ( $f, $prefix, $suffix, $separator, $column ) =
4115 _condensed_generator_defaults($format);
4118 if ($format eq 'latex') {
4121 $separator = " & \n";
4123 sub { my ($d,$a,$s,$w) = @_;
4124 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4126 }elsif ( $format eq 'html' ) {
4130 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4132 sub { my ($d,$a,$s,$w) = @_;
4133 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4142 # my $r = &{$f->{fields}->[$i]}(@args);
4143 # $r .= ' Total' unless $i;
4145 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4147 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4148 map { $f->{$_}->[$i] } qw(align span width)
4152 $prefix. join( $separator, @result ). $suffix;
4157 =item total_line_generator FORMAT
4159 Returns a coderef used for generation of invoice total line items for this
4160 usage_class. FORMAT is either html or latex
4164 # should not be used: will have issues with hash element names (description vs
4165 # total_item and amount vs total_amount -- another array of functions?
4167 sub _condensed_total_line_generator {
4168 my ( $self, $format ) = ( shift, shift );
4170 my ( $f, $prefix, $suffix, $separator, $column ) =
4171 _condensed_generator_defaults($format);
4174 if ($format eq 'latex') {
4177 $separator = " & \n";
4179 sub { my ($d,$a,$s,$w) = @_;
4180 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4182 }elsif ( $format eq 'html' ) {
4186 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4188 sub { my ($d,$a,$s,$w) = @_;
4189 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4198 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4200 &{$column}( &{$f->{fields}->[$i]}(@args),
4201 map { $f->{$_}->[$i] } qw(align span width)
4205 $prefix. join( $separator, @result ). $suffix;
4210 #sub _items_extra_usage_sections {
4212 # my $escape = shift;
4214 # my %sections = ();
4216 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4217 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4219 # next unless $cust_bill_pkg->pkgnum > 0;
4221 # foreach my $section ( keys %usage_class ) {
4223 # my $usage = $cust_bill_pkg->usage($section);
4225 # next unless $usage && $usage > 0;
4227 # $sections{$section} ||= 0;
4228 # $sections{$section} += $usage;
4234 # map { { 'description' => &{$escape}($_),
4235 # 'subtotal' => $sections{$_},
4236 # 'summarized' => '',
4237 # 'tax_section' => '',
4240 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4244 sub _items_extra_usage_sections {
4246 my $conf = $self->conf;
4254 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4256 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4257 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4258 next unless $cust_bill_pkg->pkgnum > 0;
4260 foreach my $classnum ( keys %usage_class ) {
4261 my $section = $usage_class{$classnum}->classname;
4262 $classnums{$section} = $classnum;
4264 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4265 my $amount = $detail->amount;
4266 next unless $amount && $amount > 0;
4268 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4269 $sections{$section}{amount} += $amount; #subtotal
4270 $sections{$section}{calls}++;
4271 $sections{$section}{duration} += $detail->duration;
4273 my $desc = $detail->regionname;
4274 my $description = $desc;
4275 $description = substr($desc, 0, $maxlength). '...'
4276 if $format eq 'latex' && length($desc) > $maxlength;
4278 $lines{$section}{$desc} ||= {
4279 description => &{$escape}($description),
4280 #pkgpart => $part_pkg->pkgpart,
4281 pkgnum => $cust_bill_pkg->pkgnum,
4286 #unit_amount => $cust_bill_pkg->unitrecur,
4287 quantity => $cust_bill_pkg->quantity,
4288 product_code => 'N/A',
4289 ext_description => [],
4292 $lines{$section}{$desc}{amount} += $amount;
4293 $lines{$section}{$desc}{calls}++;
4294 $lines{$section}{$desc}{duration} += $detail->duration;
4300 my %sectionmap = ();
4301 foreach (keys %sections) {
4302 my $usage_class = $usage_class{$classnums{$_}};
4303 $sectionmap{$_} = { 'description' => &{$escape}($_),
4304 'amount' => $sections{$_}{amount}, #subtotal
4305 'calls' => $sections{$_}{calls},
4306 'duration' => $sections{$_}{duration},
4308 'tax_section' => '',
4309 'sort_weight' => $usage_class->weight,
4310 ( $usage_class->format
4311 ? ( map { $_ => $usage_class->$_($format) }
4312 qw( description_generator header_generator total_generator total_line_generator )
4319 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4323 foreach my $section ( keys %lines ) {
4324 foreach my $line ( keys %{$lines{$section}} ) {
4325 my $l = $lines{$section}{$line};
4326 $l->{section} = $sectionmap{$section};
4327 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4328 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4333 return(\@sections, \@lines);
4339 my $end = $self->_date;
4341 # start at date of previous invoice + 1 second or 0 if no previous invoice
4342 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4343 $start = 0 if !$start;
4346 my $cust_main = $self->cust_main;
4347 my @pkgs = $cust_main->all_pkgs;
4348 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4351 foreach my $pkg ( @pkgs ) {
4352 my @h_cust_svc = $pkg->h_cust_svc($end);
4353 foreach my $h_cust_svc ( @h_cust_svc ) {
4354 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4355 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4357 my $inserted = $h_cust_svc->date_inserted;
4358 my $deleted = $h_cust_svc->date_deleted;
4359 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4361 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4363 # DID either activated or ported in; cannot be both for same DID simultaneously
4364 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4365 && (!$phone_inserted->lnp_status
4366 || $phone_inserted->lnp_status eq ''
4367 || $phone_inserted->lnp_status eq 'native')) {
4370 else { # this one not so clean, should probably move to (h_)svc_phone
4371 my $phone_portedin = qsearchs( 'h_svc_phone',
4372 { 'svcnum' => $h_cust_svc->svcnum,
4373 'lnp_status' => 'portedin' },
4374 FS::h_svc_phone->sql_h_searchs($end),
4376 $num_portedin++ if $phone_portedin;
4379 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4380 if($deleted >= $start && $deleted <= $end && $phone_deleted
4381 && (!$phone_deleted->lnp_status
4382 || $phone_deleted->lnp_status ne 'portingout')) {
4385 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4386 && $phone_deleted->lnp_status
4387 && $phone_deleted->lnp_status eq 'portingout') {
4391 # increment usage minutes
4392 if ( $phone_inserted ) {
4393 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4394 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4397 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4400 # don't look at this service again
4401 push @seen, $h_cust_svc->svcnum;
4405 $minutes = sprintf("%d", $minutes);
4406 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4407 . "$num_deactivated Ported-Out: $num_portedout ",
4408 "Total Minutes: $minutes");
4411 sub _items_accountcode_cdr {
4416 my $section = { 'amount' => 0,
4419 'sort_weight' => '',
4421 'description' => 'Usage by Account Code',
4427 my %accountcodes = ();
4429 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4430 next unless $cust_bill_pkg->pkgnum > 0;
4432 my @header = $cust_bill_pkg->details_header;
4433 next unless scalar(@header);
4434 $section->{'header'} = join(',',@header);
4436 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4438 $section->{'header'} = $detail->formatted('format' => $format)
4439 if($detail->detail eq $section->{'header'});
4441 my $accountcode = $detail->accountcode;
4442 next unless $accountcode;
4444 my $amount = $detail->amount;
4445 next unless $amount && $amount > 0;
4447 $accountcodes{$accountcode} ||= {
4448 description => $accountcode,
4455 product_code => 'N/A',
4456 section => $section,
4457 ext_description => [ $section->{'header'} ],
4461 $section->{'amount'} += $amount;
4462 $accountcodes{$accountcode}{'amount'} += $amount;
4463 $accountcodes{$accountcode}{calls}++;
4464 $accountcodes{$accountcode}{duration} += $detail->duration;
4465 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4469 foreach my $l ( values %accountcodes ) {
4470 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4471 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4472 foreach my $sorted_detail ( @sorted_detail ) {
4473 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4475 delete $l->{detail_temp};
4479 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4481 return ($section,\@sorted_lines);
4484 sub _items_svc_phone_sections {
4486 my $conf = $self->conf;
4494 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4496 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4497 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4499 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4500 next unless $cust_bill_pkg->pkgnum > 0;
4502 my @header = $cust_bill_pkg->details_header;
4503 next unless scalar(@header);
4505 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4507 my $phonenum = $detail->phonenum;
4508 next unless $phonenum;
4510 my $amount = $detail->amount;
4511 next unless $amount && $amount > 0;
4513 $sections{$phonenum} ||= { 'amount' => 0,
4516 'sort_weight' => -1,
4517 'phonenum' => $phonenum,
4519 $sections{$phonenum}{amount} += $amount; #subtotal
4520 $sections{$phonenum}{calls}++;
4521 $sections{$phonenum}{duration} += $detail->duration;
4523 my $desc = $detail->regionname;
4524 my $description = $desc;
4525 $description = substr($desc, 0, $maxlength). '...'
4526 if $format eq 'latex' && length($desc) > $maxlength;
4528 $lines{$phonenum}{$desc} ||= {
4529 description => &{$escape}($description),
4530 #pkgpart => $part_pkg->pkgpart,
4538 product_code => 'N/A',
4539 ext_description => [],
4542 $lines{$phonenum}{$desc}{amount} += $amount;
4543 $lines{$phonenum}{$desc}{calls}++;
4544 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4546 my $line = $usage_class{$detail->classnum}->classname;
4547 $sections{"$phonenum $line"} ||=
4551 'sort_weight' => $usage_class{$detail->classnum}->weight,
4552 'phonenum' => $phonenum,
4553 'header' => [ @header ],
4555 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4556 $sections{"$phonenum $line"}{calls}++;
4557 $sections{"$phonenum $line"}{duration} += $detail->duration;
4559 $lines{"$phonenum $line"}{$desc} ||= {
4560 description => &{$escape}($description),
4561 #pkgpart => $part_pkg->pkgpart,
4569 product_code => 'N/A',
4570 ext_description => [],
4573 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4574 $lines{"$phonenum $line"}{$desc}{calls}++;
4575 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4576 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4577 $detail->formatted('format' => $format);
4582 my %sectionmap = ();
4583 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4584 foreach ( keys %sections ) {
4585 my @header = @{ $sections{$_}{header} || [] };
4587 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4588 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4589 my $usage_class = $summary ? $simple : $usage_simple;
4590 my $ending = $summary ? ' usage charges' : '';
4593 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4595 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4596 'amount' => $sections{$_}{amount}, #subtotal
4597 'calls' => $sections{$_}{calls},
4598 'duration' => $sections{$_}{duration},
4600 'tax_section' => '',
4601 'phonenum' => $sections{$_}{phonenum},
4602 'sort_weight' => $sections{$_}{sort_weight},
4603 'post_total' => $summary, #inspire pagebreak
4605 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4606 qw( description_generator
4609 total_line_generator
4616 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4617 $a->{sort_weight} <=> $b->{sort_weight}
4622 foreach my $section ( keys %lines ) {
4623 foreach my $line ( keys %{$lines{$section}} ) {
4624 my $l = $lines{$section}{$line};
4625 $l->{section} = $sectionmap{$section};
4626 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4627 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4632 if($conf->exists('phone_usage_class_summary')) {
4633 # this only works with Latex
4637 # after this, we'll have only two sections per DID:
4638 # Calls Summary and Calls Detail
4639 foreach my $section ( @sections ) {
4640 if($section->{'post_total'}) {
4641 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4642 $section->{'total_line_generator'} = sub { '' };
4643 $section->{'total_generator'} = sub { '' };
4644 $section->{'header_generator'} = sub { '' };
4645 $section->{'description_generator'} = '';
4646 push @newsections, $section;
4647 my %calls_detail = %$section;
4648 $calls_detail{'post_total'} = '';
4649 $calls_detail{'sort_weight'} = '';
4650 $calls_detail{'description_generator'} = sub { '' };
4651 $calls_detail{'header_generator'} = sub {
4652 return ' & Date/Time & Called Number & Duration & Price'
4653 if $format eq 'latex';
4656 $calls_detail{'description'} = 'Calls Detail: '
4657 . $section->{'phonenum'};
4658 push @newsections, \%calls_detail;
4662 # after this, each usage class is collapsed/summarized into a single
4663 # line under the Calls Summary section
4664 foreach my $newsection ( @newsections ) {
4665 if($newsection->{'post_total'}) { # this means Calls Summary
4666 foreach my $section ( @sections ) {
4667 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4668 && !$section->{'post_total'});
4669 my $newdesc = $section->{'description'};
4670 my $tn = $section->{'phonenum'};
4671 $newdesc =~ s/$tn//g;
4672 my $line = { ext_description => [],
4676 calls => $section->{'calls'},
4677 section => $newsection,
4678 duration => $section->{'duration'},
4679 description => $newdesc,
4680 amount => sprintf("%.2f",$section->{'amount'}),
4681 product_code => 'N/A',
4683 push @newlines, $line;
4688 # after this, Calls Details is populated with all CDRs
4689 foreach my $newsection ( @newsections ) {
4690 if(!$newsection->{'post_total'}) { # this means Calls Details
4691 foreach my $line ( @lines ) {
4692 next unless (scalar(@{$line->{'ext_description'}}) &&
4693 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4695 my @extdesc = @{$line->{'ext_description'}};
4697 foreach my $extdesc ( @extdesc ) {
4698 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4699 push @newextdesc, $extdesc;
4701 $line->{'ext_description'} = \@newextdesc;
4702 $line->{'section'} = $newsection;
4703 push @newlines, $line;
4708 return(\@newsections, \@newlines);
4711 return(\@sections, \@lines);
4715 sub _items { # seems to be unused
4718 #my @display = scalar(@_)
4720 # : qw( _items_previous _items_pkg );
4721 # #: qw( _items_pkg );
4722 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4723 my @display = qw( _items_previous _items_pkg );
4726 foreach my $display ( @display ) {
4727 push @b, $self->$display(@_);
4732 sub _items_previous {
4734 my $conf = $self->conf;
4735 my $cust_main = $self->cust_main;
4736 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4738 foreach ( @pr_cust_bill ) {
4739 my $date = $conf->exists('invoice_show_prior_due_date')
4740 ? 'due '. $_->due_date2str($date_format)
4741 : time2str($date_format, $_->_date);
4743 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4744 #'pkgpart' => 'N/A',
4746 'amount' => sprintf("%.2f", $_->owed),
4752 # 'description' => 'Previous Balance',
4753 # #'pkgpart' => 'N/A',
4754 # 'pkgnum' => 'N/A',
4755 # 'amount' => sprintf("%10.2f", $pr_total ),
4756 # 'ext_description' => [ map {
4757 # "Invoice ". $_->invnum.
4758 # " (". time2str("%x",$_->_date). ") ".
4759 # sprintf("%10.2f", $_->owed)
4760 # } @pr_cust_bill ],
4765 =item _items_pkg [ OPTIONS ]
4767 Return line item hashes for each package item on this invoice. Nearly
4770 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4772 The only OPTIONS accepted is 'section', which may point to a hashref
4773 with a key named 'condensed', which may have a true value. If it
4774 does, this method tries to merge identical items into items with
4775 'quantity' equal to the number of items (not the sum of their
4776 separate quantities, for some reason).
4784 warn "$me _items_pkg searching for all package line items\n"
4787 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4789 warn "$me _items_pkg filtering line items\n"
4791 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4793 if ($options{section} && $options{section}->{condensed}) {
4795 warn "$me _items_pkg condensing section\n"
4799 local $Storable::canonical = 1;
4800 foreach ( @items ) {
4802 delete $item->{ref};
4803 delete $item->{ext_description};
4804 my $key = freeze($item);
4805 $itemshash{$key} ||= 0;
4806 $itemshash{$key} ++; # += $item->{quantity};
4808 @items = sort { $a->{description} cmp $b->{description} }
4809 map { my $i = thaw($_);
4810 $i->{quantity} = $itemshash{$_};
4812 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4818 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4825 return 0 unless $a->itemdesc cmp $b->itemdesc;
4826 return -1 if $b->itemdesc eq 'Tax';
4827 return 1 if $a->itemdesc eq 'Tax';
4828 return -1 if $b->itemdesc eq 'Other surcharges';
4829 return 1 if $a->itemdesc eq 'Other surcharges';
4830 $a->itemdesc cmp $b->itemdesc;
4835 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4836 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4839 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4841 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4842 list of hashrefs describing the line items they generate on the invoice.
4844 OPTIONS may include:
4846 format: the invoice format.
4848 escape_function: the function used to escape strings.
4850 DEPRECATED? (expensive, mostly unused?)
4851 format_function: the function used to format CDRs.
4853 section: a hashref containing 'description'; if this is present,
4854 cust_bill_pkg_display records not belonging to this section are
4857 multisection: a flag indicating that this is a multisection invoice,
4858 which does something complicated.
4860 multilocation: a flag to display the location label for the package.
4862 Returns a list of hashrefs, each of which may contain:
4864 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4865 ext_description, which is an arrayref of detail lines to show below
4870 sub _items_cust_bill_pkg {
4872 my $conf = $self->conf;
4873 my $cust_bill_pkgs = shift;
4876 my $format = $opt{format} || '';
4877 my $escape_function = $opt{escape_function} || sub { shift };
4878 my $format_function = $opt{format_function} || '';
4879 my $no_usage = $opt{no_usage} || '';
4880 my $unsquelched = $opt{unsquelched} || ''; #unused
4881 my $section = $opt{section}->{description} if $opt{section};
4882 my $summary_page = $opt{summary_page} || ''; #unused
4883 my $multilocation = $opt{multilocation} || '';
4884 my $multisection = $opt{multisection} || '';
4885 my $discount_show_always = 0;
4887 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4890 my ($s, $r, $u) = ( undef, undef, undef );
4891 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4894 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4895 if ( $_ && !$cust_bill_pkg->hidden ) {
4896 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4897 $_->{amount} =~ s/^\-0\.00$/0.00/;
4898 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4900 if $_->{amount} != 0
4901 || $discount_show_always
4902 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4903 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4909 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4910 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4913 foreach my $display ( grep { defined($section)
4914 ? $_->section eq $section
4917 #grep { !$_->summary || !$summary_page } # bunk!
4918 grep { !$_->summary || $multisection }
4919 $cust_bill_pkg->cust_bill_pkg_display
4923 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4924 $display->billpkgdisplaynum. "\n"
4927 my $type = $display->type;
4929 my $desc = $cust_bill_pkg->desc;
4930 $desc = substr($desc, 0, $maxlength). '...'
4931 if $format eq 'latex' && length($desc) > $maxlength;
4933 my %details_opt = ( 'format' => $format,
4934 'escape_function' => $escape_function,
4935 'format_function' => $format_function,
4936 'no_usage' => $opt{'no_usage'},
4939 if ( $cust_bill_pkg->pkgnum > 0 ) {
4941 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4944 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4946 # start/end dates for invoice formats that do nonstandard
4948 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4950 if ( (!$type || $type eq 'S')
4951 && ( $cust_bill_pkg->setup != 0
4952 || $cust_bill_pkg->setup_show_zero
4957 warn "$me _items_cust_bill_pkg adding setup\n"
4960 my $description = $desc;
4961 $description .= ' Setup'
4962 if $cust_bill_pkg->recur != 0
4963 || $discount_show_always
4964 || $cust_bill_pkg->recur_show_zero;
4967 unless ( $cust_pkg->part_pkg->hide_svc_detail
4968 || $cust_bill_pkg->hidden )
4971 push @d, map &{$escape_function}($_),
4972 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4973 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4975 if ( $multilocation ) {
4976 my $loc = $cust_pkg->location_label;
4977 $loc = substr($loc, 0, $maxlength). '...'
4978 if $format eq 'latex' && length($loc) > $maxlength;
4979 push @d, &{$escape_function}($loc);
4982 } #unless hiding service details
4984 push @d, $cust_bill_pkg->details(%details_opt)
4985 if $cust_bill_pkg->recur == 0;
4987 if ( $cust_bill_pkg->hidden ) {
4988 $s->{amount} += $cust_bill_pkg->setup;
4989 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4990 push @{ $s->{ext_description} }, @d;
4994 description => $description,
4995 #pkgpart => $part_pkg->pkgpart,
4996 pkgnum => $cust_bill_pkg->pkgnum,
4997 amount => $cust_bill_pkg->setup,
4998 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4999 unit_amount => $cust_bill_pkg->unitsetup,
5000 quantity => $cust_bill_pkg->quantity,
5001 ext_description => \@d,
5007 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5009 $cust_bill_pkg->recur != 0
5010 || $cust_bill_pkg->setup == 0
5011 || $discount_show_always
5012 || $cust_bill_pkg->recur_show_zero
5017 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5020 my $is_summary = $display->summary;
5021 my $description = ($is_summary && $type && $type eq 'U')
5022 ? "Usage charges" : $desc;
5025 $conf->exists('disable_line_item_date_ranges')
5026 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5029 my $date_style = $conf->config('cust_bill-line_item-date_style');
5030 if ( defined($date_style) && $date_style eq 'month_of' ) {
5031 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5033 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5034 " - ". time2str($date_format, $cust_bill_pkg->edate);
5036 $description .= " ($time_period)";
5040 my @seconds = (); # for display of usage info
5042 #at least until cust_bill_pkg has "past" ranges in addition to
5043 #the "future" sdate/edate ones... see #3032
5044 my @dates = ( $self->_date );
5045 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5046 push @dates, $prev->sdate if $prev;
5047 push @dates, undef if !$prev;
5049 unless ( $cust_pkg->part_pkg->hide_svc_detail
5050 || $cust_bill_pkg->itemdesc
5051 || $cust_bill_pkg->hidden
5052 || $is_summary && $type && $type eq 'U' )
5055 warn "$me _items_cust_bill_pkg adding service details\n"
5058 push @d, map &{$escape_function}($_),
5059 $cust_pkg->h_labels_short(@dates, 'I')
5060 #$cust_bill_pkg->edate,
5061 #$cust_bill_pkg->sdate)
5062 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5064 warn "$me _items_cust_bill_pkg done adding service details\n"
5067 if ( $multilocation ) {
5068 my $loc = $cust_pkg->location_label;
5069 $loc = substr($loc, 0, $maxlength). '...'
5070 if $format eq 'latex' && length($loc) > $maxlength;
5071 push @d, &{$escape_function}($loc);
5074 # Display of seconds_since_sqlradacct:
5075 # On the invoice, when processing @detail_items, look for a field
5076 # named 'seconds'. This will contain total seconds for each
5077 # service, in the same order as @ext_description. For services
5078 # that don't support this it will show undef.
5079 if ( $conf->exists('svc_acct-usage_seconds')
5080 and ! $cust_bill_pkg->pkgpart_override ) {
5081 foreach my $cust_svc (
5082 $cust_pkg->h_cust_svc(@dates, 'I')
5085 # eval because not having any part_export_usage exports
5086 # is a fatal error, last_bill/_date because that's how
5087 # sqlradius_hour billing does it
5089 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5091 push @seconds, $sec;
5093 } #if svc_acct-usage_seconds
5097 unless ( $is_summary ) {
5098 warn "$me _items_cust_bill_pkg adding details\n"
5101 #instead of omitting details entirely in this case (unwanted side
5102 # effects), just omit CDRs
5103 $details_opt{'no_usage'} = 1
5104 if $type && $type eq 'R';
5106 push @d, $cust_bill_pkg->details(%details_opt);
5109 warn "$me _items_cust_bill_pkg calculating amount\n"
5114 $amount = $cust_bill_pkg->recur;
5115 } elsif ($type eq 'R') {
5116 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5117 } elsif ($type eq 'U') {
5118 $amount = $cust_bill_pkg->usage;
5121 if ( !$type || $type eq 'R' ) {
5123 warn "$me _items_cust_bill_pkg adding recur\n"
5126 if ( $cust_bill_pkg->hidden ) {
5127 $r->{amount} += $amount;
5128 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5129 push @{ $r->{ext_description} }, @d;
5132 description => $description,
5133 #pkgpart => $part_pkg->pkgpart,
5134 pkgnum => $cust_bill_pkg->pkgnum,
5136 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5137 unit_amount => $cust_bill_pkg->unitrecur,
5138 quantity => $cust_bill_pkg->quantity,
5140 ext_description => \@d,
5142 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5145 } else { # $type eq 'U'
5147 warn "$me _items_cust_bill_pkg adding usage\n"
5150 if ( $cust_bill_pkg->hidden ) {
5151 $u->{amount} += $amount;
5152 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5153 push @{ $u->{ext_description} }, @d;
5156 description => $description,
5157 #pkgpart => $part_pkg->pkgpart,
5158 pkgnum => $cust_bill_pkg->pkgnum,
5160 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5161 unit_amount => $cust_bill_pkg->unitrecur,
5162 quantity => $cust_bill_pkg->quantity,
5164 ext_description => \@d,
5169 } # recurring or usage with recurring charge
5171 } else { #pkgnum tax or one-shot line item (??)
5173 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5176 if ( $cust_bill_pkg->setup != 0 ) {
5178 'description' => $desc,
5179 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5182 if ( $cust_bill_pkg->recur != 0 ) {
5184 'description' => "$desc (".
5185 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5186 time2str($date_format, $cust_bill_pkg->edate). ')',
5187 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5195 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5196 && $conf->exists('discount-show-always'));
5200 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5202 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5203 $_->{amount} =~ s/^\-0\.00$/0.00/;
5204 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5206 if $_->{amount} != 0
5207 || $discount_show_always
5208 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5209 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5213 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5220 sub _items_credits {
5221 my( $self, %opt ) = @_;
5222 my $trim_len = $opt{'trim_len'} || 60;
5226 foreach ( $self->cust_credited ) {
5228 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5230 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5231 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5232 $reason = " ($reason) " if $reason;
5235 #'description' => 'Credit ref\#'. $_->crednum.
5236 # " (". time2str("%x",$_->cust_credit->_date) .")".
5238 'description' => $self->mt('Credit applied').' '.
5239 time2str($date_format,$_->cust_credit->_date). $reason,
5240 'amount' => sprintf("%.2f",$_->amount),
5248 sub _items_payments {
5252 #get & print payments
5253 foreach ( $self->cust_bill_pay ) {
5255 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5258 'description' => $self->mt('Payment received').' '.
5259 time2str($date_format,$_->cust_pay->_date ),
5260 'amount' => sprintf("%.2f", $_->amount )
5268 =item _items_discounts_avail
5270 Returns an array of line item hashrefs representing available term discounts
5271 for this invoice. This makes the same assumptions that apply to term
5272 discounts in general: that the package is billed monthly, at a flat rate,
5273 with no usage charges. A prorated first month will be handled, as will
5274 a setup fee if the discount is allowed to apply to setup fees.
5278 sub _items_discounts_avail {
5280 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5282 my %plans = $self->discount_plans;
5284 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5288 my $plan = $plans{$months};
5290 my $term_total = sprintf('%.2f', $plan->discounted_total);
5291 my $percent = sprintf('%.0f',
5292 100 * (1 - $term_total / $plan->base_total) );
5293 my $permonth = sprintf('%.2f', $term_total / $months);
5294 my $detail = $self->mt('discount on item'). ' '.
5295 join(', ', map { "#$_" } $plan->pkgnums)
5298 # discounts for non-integer months don't work anyway
5299 $months = sprintf("%d", $months);
5302 description => $self->mt('Save [_1]% by paying for [_2] months',
5304 amount => $self->mt('[_1] ([_2] per month)',
5305 $term_total, $money_char.$permonth),
5306 ext_description => ($detail || ''),
5309 sort { $b <=> $a } keys %plans;
5313 =item call_details [ OPTION => VALUE ... ]
5315 Returns an array of CSV strings representing the call details for this invoice
5316 The only option available is the boolean prepend_billed_number
5321 my ($self, %opt) = @_;
5323 my $format_function = sub { shift };
5325 if ($opt{prepend_billed_number}) {
5326 $format_function = sub {
5330 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5335 my @details = map { $_->details( 'format_function' => $format_function,
5336 'escape_function' => sub{ return() },
5340 $self->cust_bill_pkg;
5341 my $header = $details[0];
5342 ( $header, grep { $_ ne $header } @details );
5352 =item process_reprint
5356 sub process_reprint {
5357 process_re_X('print', @_);
5360 =item process_reemail
5364 sub process_reemail {
5365 process_re_X('email', @_);
5373 process_re_X('fax', @_);
5381 process_re_X('ftp', @_);
5388 sub process_respool {
5389 process_re_X('spool', @_);
5392 use Storable qw(thaw);
5396 my( $method, $job ) = ( shift, shift );
5397 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5399 my $param = thaw(decode_base64(shift));
5400 warn Dumper($param) if $DEBUG;
5411 my($method, $job, %param ) = @_;
5413 warn "re_X $method for job $job with param:\n".
5414 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5417 #some false laziness w/search/cust_bill.html
5419 my $orderby = 'ORDER BY cust_bill._date';
5421 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5423 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5425 my @cust_bill = qsearch( {
5426 #'select' => "cust_bill.*",
5427 'table' => 'cust_bill',
5428 'addl_from' => $addl_from,
5430 'extra_sql' => $extra_sql,
5431 'order_by' => $orderby,
5435 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5437 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5440 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5441 foreach my $cust_bill ( @cust_bill ) {
5442 $cust_bill->$method();
5444 if ( $job ) { #progressbar foo
5446 if ( time - $min_sec > $last ) {
5447 my $error = $job->update_statustext(
5448 int( 100 * $num / scalar(@cust_bill) )
5450 die $error if $error;
5461 =head1 CLASS METHODS
5467 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5472 my ($class, $start, $end) = @_;
5474 $class->paid_sql($start, $end). ' - '.
5475 $class->credited_sql($start, $end);
5480 Returns an SQL fragment to retreive the net amount (charged minus credited).
5485 my ($class, $start, $end) = @_;
5486 'charged - '. $class->credited_sql($start, $end);
5491 Returns an SQL fragment to retreive the amount paid against this invoice.
5496 my ($class, $start, $end) = @_;
5497 $start &&= "AND cust_bill_pay._date <= $start";
5498 $end &&= "AND cust_bill_pay._date > $end";
5499 $start = '' unless defined($start);
5500 $end = '' unless defined($end);
5501 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5502 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5507 Returns an SQL fragment to retreive the amount credited against this invoice.
5512 my ($class, $start, $end) = @_;
5513 $start &&= "AND cust_credit_bill._date <= $start";
5514 $end &&= "AND cust_credit_bill._date > $end";
5515 $start = '' unless defined($start);
5516 $end = '' unless defined($end);
5517 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5518 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5523 Returns an SQL fragment to retrieve the due date of an invoice.
5524 Currently only supported on PostgreSQL.
5529 my $conf = new FS::Conf;
5533 cust_bill.invoice_terms,
5534 cust_main.invoice_terms,
5535 \''.($conf->config('invoice_default_terms') || '').'\'
5536 ), E\'Net (\\\\d+)\'
5538 ) * 86400 + cust_bill._date'
5541 =item search_sql_where HASHREF
5543 Class method which returns an SQL WHERE fragment to search for parameters
5544 specified in HASHREF. Valid parameters are
5550 List reference of start date, end date, as UNIX timestamps.
5560 List reference of charged limits (exclusive).
5564 List reference of charged limits (exclusive).
5568 flag, return open invoices only
5572 flag, return net invoices only
5576 =item newest_percust
5580 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5584 sub search_sql_where {
5585 my($class, $param) = @_;
5587 warn "$me search_sql_where called with params: \n".
5588 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5594 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5595 push @search, "cust_main.agentnum = $1";
5599 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5600 push @search, "cust_bill.custnum = $1";
5604 if ( $param->{_date} ) {
5605 my($beginning, $ending) = @{$param->{_date}};
5607 push @search, "cust_bill._date >= $beginning",
5608 "cust_bill._date < $ending";
5612 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5613 push @search, "cust_bill.invnum >= $1";
5615 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5616 push @search, "cust_bill.invnum <= $1";
5620 if ( $param->{charged} ) {
5621 my @charged = ref($param->{charged})
5622 ? @{ $param->{charged} }
5623 : ($param->{charged});
5625 push @search, map { s/^charged/cust_bill.charged/; $_; }
5629 my $owed_sql = FS::cust_bill->owed_sql;
5632 if ( $param->{owed} ) {
5633 my @owed = ref($param->{owed})
5634 ? @{ $param->{owed} }
5636 push @search, map { s/^owed/$owed_sql/; $_; }
5641 push @search, "0 != $owed_sql"
5642 if $param->{'open'};
5643 push @search, '0 != '. FS::cust_bill->net_sql
5647 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5648 if $param->{'days'};
5651 if ( $param->{'newest_percust'} ) {
5653 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5654 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5656 my @newest_where = map { my $x = $_;
5657 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5660 grep ! /^cust_main./, @search;
5661 my $newest_where = scalar(@newest_where)
5662 ? ' AND '. join(' AND ', @newest_where)
5666 push @search, "cust_bill._date = (
5667 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5668 WHERE newest_cust_bill.custnum = cust_bill.custnum
5674 #promised_date - also has an option to accept nulls
5675 if ( $param->{promised_date} ) {
5676 my($beginning, $ending, $null) = @{$param->{promised_date}};
5678 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5679 "cust_bill.promised_date < $ending )" .
5680 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5683 #agent virtualization
5684 my $curuser = $FS::CurrentUser::CurrentUser;
5685 if ( $curuser->username eq 'fs_queue'
5686 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5688 my $newuser = qsearchs('access_user', {
5689 'username' => $username,
5693 $curuser = $newuser;
5695 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5698 push @search, $curuser->agentnums_sql;
5700 join(' AND ', @search );
5712 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5713 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base