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 my $cust_main = $self->cust_main;
1319 return 'N/A' unless ! $agentnums
1320 or grep { $_ == $cust_main->agentnum } @$agentnums;
1323 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1325 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1326 $conf->config('invoice_from', $cust_main->agentnum );
1329 'template' => $template,
1330 'invoice_from' => $invoice_from,
1331 'notice_name' => ( $notice_name || 'Invoice' ),
1334 my @invoicing_list = $cust_main->invoicing_list;
1336 #$self->email_invoice(\%opt)
1338 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1339 && ! $self->invoice_noemail;
1341 #$self->print_invoice(\%opt)
1343 if grep { $_ eq 'POST' } @invoicing_list; #postal
1345 $self->fax_invoice(\%opt)
1346 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1352 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1354 Emails this invoice.
1356 Options can be passed as a hashref (recommended) or as a list of up to
1357 two values for templatename and invoice_from.
1359 I<template>, if specified, is the name of a suffix for alternate invoices.
1361 I<invoice_from>, if specified, overrides the default email invoice From: address.
1363 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1367 sub queueable_email {
1370 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1371 or die "invalid invoice number: " . $opt{invnum};
1373 my %args = ( 'template' => $opt{template} );
1374 $args{$_} = $opt{$_}
1375 foreach grep { exists($opt{$_}) && $opt{$_} }
1376 qw( invoice_from notice_name no_coupon );
1378 my $error = $self->email( \%args );
1379 die $error if $error;
1383 #sub email_invoice {
1386 return if $self->hide;
1387 my $conf = $self->conf;
1389 my( $template, $invoice_from, $notice_name, $no_coupon );
1392 $template = $opt->{'template'} || '';
1393 $invoice_from = $opt->{'invoice_from'};
1394 $notice_name = $opt->{'notice_name'} || 'Invoice';
1395 $no_coupon = $opt->{'no_coupon'} || 0;
1397 $template = scalar(@_) ? shift : '';
1398 $invoice_from = shift if scalar(@_);
1399 $notice_name = 'Invoice';
1403 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1404 $conf->config('invoice_from', $self->cust_main->agentnum );
1406 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1407 $self->cust_main->invoicing_list;
1409 if ( ! @invoicing_list ) { #no recipients
1410 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1411 die 'No recipients for customer #'. $self->custnum;
1413 #default: better to notify this person than silence
1414 @invoicing_list = ($invoice_from);
1418 my $subject = $self->email_subject($template);
1420 my $error = send_email(
1421 $self->generate_email(
1422 'from' => $invoice_from,
1423 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1424 'subject' => $subject,
1425 'template' => $template,
1426 'notice_name' => $notice_name,
1427 'no_coupon' => $no_coupon,
1430 die "can't email invoice: $error\n" if $error;
1431 #die "$error\n" if $error;
1437 my $conf = $self->conf;
1439 #my $template = scalar(@_) ? shift : '';
1442 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1445 my $cust_main = $self->cust_main;
1446 my $name = $cust_main->name;
1447 my $name_short = $cust_main->name_short;
1448 my $invoice_number = $self->invnum;
1449 my $invoice_date = $self->_date_pretty;
1451 eval qq("$subject");
1454 =item lpr_data HASHREF | [ TEMPLATE ]
1456 Returns the postscript or plaintext for this invoice as an arrayref.
1458 Options can be passed as a hashref (recommended) or as a single optional value
1461 I<template>, if specified, is the name of a suffix for alternate invoices.
1463 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1469 my $conf = $self->conf;
1470 my( $template, $notice_name );
1473 $template = $opt->{'template'} || '';
1474 $notice_name = $opt->{'notice_name'} || 'Invoice';
1476 $template = scalar(@_) ? shift : '';
1477 $notice_name = 'Invoice';
1481 'template' => $template,
1482 'notice_name' => $notice_name,
1485 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1486 [ $self->$method( \%opt ) ];
1489 =item print HASHREF | [ TEMPLATE ]
1491 Prints this invoice.
1493 Options can be passed as a hashref (recommended) or as a single optional
1496 I<template>, if specified, is the name of a suffix for alternate invoices.
1498 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1502 #sub print_invoice {
1505 return if $self->hide;
1506 my $conf = $self->conf;
1508 my( $template, $notice_name );
1511 $template = $opt->{'template'} || '';
1512 $notice_name = $opt->{'notice_name'} || 'Invoice';
1514 $template = scalar(@_) ? shift : '';
1515 $notice_name = 'Invoice';
1519 'template' => $template,
1520 'notice_name' => $notice_name,
1523 if($conf->exists('invoice_print_pdf')) {
1524 # Add the invoice to the current batch.
1525 $self->batch_invoice(\%opt);
1528 do_print $self->lpr_data(\%opt);
1532 =item fax_invoice HASHREF | [ TEMPLATE ]
1536 Options can be passed as a hashref (recommended) or as a single optional
1539 I<template>, if specified, is the name of a suffix for alternate invoices.
1541 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1547 return if $self->hide;
1548 my $conf = $self->conf;
1550 my( $template, $notice_name );
1553 $template = $opt->{'template'} || '';
1554 $notice_name = $opt->{'notice_name'} || 'Invoice';
1556 $template = scalar(@_) ? shift : '';
1557 $notice_name = 'Invoice';
1560 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1561 unless $conf->exists('invoice_latex');
1563 my $dialstring = $self->cust_main->getfield('fax');
1567 'template' => $template,
1568 'notice_name' => $notice_name,
1571 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1572 'dialstring' => $dialstring,
1574 die $error if $error;
1578 =item batch_invoice [ HASHREF ]
1580 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1581 isn't an open batch, one will be created.
1586 my ($self, $opt) = @_;
1587 my $bill_batch = $self->get_open_bill_batch;
1588 my $cust_bill_batch = FS::cust_bill_batch->new({
1589 batchnum => $bill_batch->batchnum,
1590 invnum => $self->invnum,
1592 return $cust_bill_batch->insert($opt);
1595 =item get_open_batch
1597 Returns the currently open batch as an FS::bill_batch object, creating a new
1598 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1603 sub get_open_bill_batch {
1605 my $conf = $self->conf;
1606 my $hashref = { status => 'O' };
1607 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1608 ? $self->cust_main->agentnum
1610 my $batch = qsearchs('bill_batch', $hashref);
1611 return $batch if $batch;
1612 $batch = FS::bill_batch->new($hashref);
1613 my $error = $batch->insert;
1614 die $error if $error;
1618 =item ftp_invoice [ TEMPLATENAME ]
1620 Sends this invoice data via FTP.
1622 TEMPLATENAME is unused?
1628 my $conf = $self->conf;
1629 my $template = scalar(@_) ? shift : '';
1632 'protocol' => 'ftp',
1633 'server' => $conf->config('cust_bill-ftpserver'),
1634 'username' => $conf->config('cust_bill-ftpusername'),
1635 'password' => $conf->config('cust_bill-ftppassword'),
1636 'dir' => $conf->config('cust_bill-ftpdir'),
1637 'format' => $conf->config('cust_bill-ftpformat'),
1641 =item spool_invoice [ TEMPLATENAME ]
1643 Spools this invoice data (see L<FS::spool_csv>)
1645 TEMPLATENAME is unused?
1651 my $conf = $self->conf;
1652 my $template = scalar(@_) ? shift : '';
1655 'format' => $conf->config('cust_bill-spoolformat'),
1656 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1660 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1662 Like B<send>, but only sends the invoice if it is the newest open invoice for
1667 sub send_if_newest {
1672 grep { $_->owed > 0 }
1673 qsearch('cust_bill', {
1674 'custnum' => $self->custnum,
1675 #'_date' => { op=>'>', value=>$self->_date },
1676 'invnum' => { op=>'>', value=>$self->invnum },
1683 =item send_csv OPTION => VALUE, ...
1685 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1689 protocol - currently only "ftp"
1695 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1696 and YYMMDDHHMMSS is a timestamp.
1698 See L</print_csv> for a description of the output format.
1703 my($self, %opt) = @_;
1707 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1708 mkdir $spooldir, 0700 unless -d $spooldir;
1710 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1711 my $file = "$spooldir/$tracctnum.csv";
1713 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1715 open(CSV, ">$file") or die "can't open $file: $!";
1723 if ( $opt{protocol} eq 'ftp' ) {
1724 eval "use Net::FTP;";
1726 $net = Net::FTP->new($opt{server}) or die @$;
1728 die "unknown protocol: $opt{protocol}";
1731 $net->login( $opt{username}, $opt{password} )
1732 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1734 $net->binary or die "can't set binary mode";
1736 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1738 $net->put($file) or die "can't put $file: $!";
1748 Spools CSV invoice data.
1754 =item format - 'default' or 'billco'
1756 =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>).
1758 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1760 =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.
1767 my($self, %opt) = @_;
1769 my $cust_main = $self->cust_main;
1771 if ( $opt{'dest'} ) {
1772 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1773 $cust_main->invoicing_list;
1774 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1775 || ! keys %invoicing_list;
1778 if ( $opt{'balanceover'} ) {
1780 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1783 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1784 mkdir $spooldir, 0700 unless -d $spooldir;
1786 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1790 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1791 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1794 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1796 open(CSV, ">>$file") or die "can't open $file: $!";
1797 flock(CSV, LOCK_EX);
1802 if ( lc($opt{'format'}) eq 'billco' ) {
1804 flock(CSV, LOCK_UN);
1809 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1812 open(CSV,">>$file") or die "can't open $file: $!";
1813 flock(CSV, LOCK_EX);
1819 flock(CSV, LOCK_UN);
1826 =item print_csv OPTION => VALUE, ...
1828 Returns CSV data for this invoice.
1832 format - 'default' or 'billco'
1834 Returns a list consisting of two scalars. The first is a single line of CSV
1835 header information for this invoice. The second is one or more lines of CSV
1836 detail information for this invoice.
1838 If I<format> is not specified or "default", the fields of the CSV file are as
1841 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1845 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1847 B<record_type> is C<cust_bill> for the initial header line only. The
1848 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1849 fields are filled in.
1851 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1852 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1855 =item invnum - invoice number
1857 =item custnum - customer number
1859 =item _date - invoice date
1861 =item charged - total invoice amount
1863 =item first - customer first name
1865 =item last - customer first name
1867 =item company - company name
1869 =item address1 - address line 1
1871 =item address2 - address line 1
1881 =item pkg - line item description
1883 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1885 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1887 =item sdate - start date for recurring fee
1889 =item edate - end date for recurring fee
1893 If I<format> is "billco", the fields of the header CSV file are as follows:
1895 +-------------------------------------------------------------------+
1896 | FORMAT HEADER FILE |
1897 |-------------------------------------------------------------------|
1898 | Field | Description | Name | Type | Width |
1899 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1900 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1901 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1902 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1903 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1904 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1905 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1906 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1907 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1908 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1909 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1910 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1911 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1912 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1913 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1914 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1915 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1916 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1917 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1918 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1919 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1920 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1921 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1922 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1923 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1924 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1925 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1926 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1927 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1928 +-------+-------------------------------+------------+------+-------+
1930 If I<format> is "billco", the fields of the detail CSV file are as follows:
1932 FORMAT FOR DETAIL FILE
1934 Field | Description | Name | Type | Width
1935 1 | N/A-Leave Empty | RC | CHAR | 2
1936 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1937 3 | Account Number | TRACCTNUM | CHAR | 15
1938 4 | Invoice Number | TRINVOICE | CHAR | 15
1939 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1940 6 | Transaction Detail | DETAILS | CHAR | 100
1941 7 | Amount | AMT | NUM* | 9
1942 8 | Line Format Control** | LNCTRL | CHAR | 2
1943 9 | Grouping Code | GROUP | CHAR | 2
1944 10 | User Defined | ACCT CODE | CHAR | 15
1949 my($self, %opt) = @_;
1951 eval "use Text::CSV_XS";
1954 my $cust_main = $self->cust_main;
1956 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1958 if ( lc($opt{'format'}) eq 'billco' ) {
1961 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1963 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1965 my( $previous_balance, @unused ) = $self->previous; #previous balance
1967 my $pmt_cr_applied = 0;
1968 $pmt_cr_applied += $_->{'amount'}
1969 foreach ( $self->_items_payments, $self->_items_credits ) ;
1971 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1974 '', # 1 | N/A-Leave Empty CHAR 2
1975 '', # 2 | N/A-Leave Empty CHAR 15
1976 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1977 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1978 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1979 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1980 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1981 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1982 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1983 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1984 '', # 10 | Ancillary Billing Information CHAR 30
1985 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1986 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1989 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1992 $duedate, # 14 | Bill Due Date CHAR 10
1994 $previous_balance, # 15 | Previous Balance NUM* 9
1995 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1996 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1997 $totaldue, # 18 | Total Amt Due NUM* 9
1998 $totaldue, # 19 | Total Amt Due NUM* 9
1999 '', # 20 | 30 Day Aging NUM* 9
2000 '', # 21 | 60 Day Aging NUM* 9
2001 '', # 22 | 90 Day Aging NUM* 9
2002 'N', # 23 | Y/N CHAR 1
2003 '', # 24 | Remittance automation CHAR 100
2004 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2005 $self->custnum, # 26 | Customer Reference Number CHAR 15
2006 '0', # 27 | Federal Tax*** NUM* 9
2007 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2008 '0', # 29 | Other Taxes & Fees*** NUM* 9
2011 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2013 my ($previous_balance) = $self->previous;
2014 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2016 ($_->{pkgnum} || ''),
2019 } $self->_items_pkg;
2022 $cust_main->agentnum,
2023 $cust_main->agent->agent,
2027 $cust_main->address1,
2028 $cust_main->address2,
2034 time2str("%x", $self->_date),
2048 time2str("%x", $self->_date),
2049 sprintf("%.2f", $self->charged),
2050 ( map { $cust_main->getfield($_) }
2051 qw( first last company address1 address2 city state zip country ) ),
2053 ) or die "can't create csv";
2056 my $header = $csv->string. "\n";
2059 if ( lc($opt{'format'}) eq 'billco' ) {
2062 foreach my $item ( $self->_items_pkg ) {
2065 '', # 1 | N/A-Leave Empty CHAR 2
2066 '', # 2 | N/A-Leave Empty CHAR 15
2067 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2068 $self->invnum, # 4 | Invoice Number CHAR 15
2069 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2070 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2071 $item->{'amount'}, # 7 | Amount NUM* 9
2072 '', # 8 | Line Format Control** CHAR 2
2073 '', # 9 | Grouping Code CHAR 2
2074 '', # 10 | User Defined CHAR 15
2077 $detail .= $csv->string. "\n";
2081 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2087 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2089 my($pkg, $setup, $recur, $sdate, $edate);
2090 if ( $cust_bill_pkg->pkgnum ) {
2092 ($pkg, $setup, $recur, $sdate, $edate) = (
2093 $cust_bill_pkg->part_pkg->pkg,
2094 ( $cust_bill_pkg->setup != 0
2095 ? sprintf("%.2f", $cust_bill_pkg->setup )
2097 ( $cust_bill_pkg->recur != 0
2098 ? sprintf("%.2f", $cust_bill_pkg->recur )
2100 ( $cust_bill_pkg->sdate
2101 ? time2str("%x", $cust_bill_pkg->sdate)
2103 ($cust_bill_pkg->edate
2104 ?time2str("%x", $cust_bill_pkg->edate)
2108 } else { #pkgnum tax
2109 next unless $cust_bill_pkg->setup != 0;
2110 $pkg = $cust_bill_pkg->desc;
2111 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2112 ( $sdate, $edate ) = ( '', '' );
2118 ( map { '' } (1..11) ),
2119 ($pkg, $setup, $recur, $sdate, $edate)
2120 ) or die "can't create csv";
2122 $detail .= $csv->string. "\n";
2128 ( $header, $detail );
2134 Pays this invoice with a compliemntary payment. If there is an error,
2135 returns the error, otherwise returns false.
2141 my $cust_pay = new FS::cust_pay ( {
2142 'invnum' => $self->invnum,
2143 'paid' => $self->owed,
2146 'payinfo' => $self->cust_main->payinfo,
2154 Attempts to pay this invoice with a credit card payment via a
2155 Business::OnlinePayment realtime gateway. See
2156 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2157 for supported processors.
2163 $self->realtime_bop( 'CC', @_ );
2168 Attempts to pay this invoice with an electronic check (ACH) payment via a
2169 Business::OnlinePayment realtime gateway. See
2170 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2171 for supported processors.
2177 $self->realtime_bop( 'ECHECK', @_ );
2182 Attempts to pay this invoice with phone bill (LEC) payment via a
2183 Business::OnlinePayment realtime gateway. See
2184 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2185 for supported processors.
2191 $self->realtime_bop( 'LEC', @_ );
2195 my( $self, $method ) = (shift,shift);
2196 my $conf = $self->conf;
2199 my $cust_main = $self->cust_main;
2200 my $balance = $cust_main->balance;
2201 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2202 $amount = sprintf("%.2f", $amount);
2203 return "not run (balance $balance)" unless $amount > 0;
2205 my $description = 'Internet Services';
2206 if ( $conf->exists('business-onlinepayment-description') ) {
2207 my $dtempl = $conf->config('business-onlinepayment-description');
2209 my $agent_obj = $cust_main->agent
2210 or die "can't retreive agent for $cust_main (agentnum ".
2211 $cust_main->agentnum. ")";
2212 my $agent = $agent_obj->agent;
2213 my $pkgs = join(', ',
2214 map { $_->part_pkg->pkg }
2215 grep { $_->pkgnum } $self->cust_bill_pkg
2217 $description = eval qq("$dtempl");
2220 $cust_main->realtime_bop($method, $amount,
2221 'description' => $description,
2222 'invnum' => $self->invnum,
2223 #this didn't do what we want, it just calls apply_payments_and_credits
2225 'apply_to_invoice' => 1,
2228 #this changes application behavior: auto payments
2229 #triggered against a specific invoice are now applied
2230 #to that invoice instead of oldest open.
2236 =item batch_card OPTION => VALUE...
2238 Adds a payment for this invoice to the pending credit card batch (see
2239 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2240 runs the payment using a realtime gateway.
2245 my ($self, %options) = @_;
2246 my $cust_main = $self->cust_main;
2248 $options{invnum} = $self->invnum;
2250 $cust_main->batch_card(%options);
2253 sub _agent_template {
2255 $self->cust_main->agent_template;
2258 sub _agent_invoice_from {
2260 $self->cust_main->agent_invoice_from;
2263 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2265 Returns an text invoice, as a list of lines.
2267 Options can be passed as a hashref (recommended) or as a list of time, template
2268 and then any key/value pairs for any other options.
2270 I<time>, if specified, is used to control the printing of overdue messages. The
2271 default is now. It isn't the date of the invoice; that's the `_date' field.
2272 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2273 L<Time::Local> and L<Date::Parse> for conversion functions.
2275 I<template>, if specified, is the name of a suffix for alternate invoices.
2277 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2283 my( $today, $template, %opt );
2285 %opt = %{ shift() };
2286 $today = delete($opt{'time'}) || '';
2287 $template = delete($opt{template}) || '';
2289 ( $today, $template, %opt ) = @_;
2292 my %params = ( 'format' => 'template' );
2293 $params{'time'} = $today if $today;
2294 $params{'template'} = $template if $template;
2295 $params{$_} = $opt{$_}
2296 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2298 $self->print_generic( %params );
2301 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2303 Internal method - returns a filename of a filled-in LaTeX template for this
2304 invoice (Note: add ".tex" to get the actual filename), and a filename of
2305 an associated logo (with the .eps extension included).
2307 See print_ps and print_pdf for methods that return PostScript and PDF output.
2309 Options can be passed as a hashref (recommended) or as a list of time, template
2310 and then any key/value pairs for any other options.
2312 I<time>, if specified, is used to control the printing of overdue messages. The
2313 default is now. It isn't the date of the invoice; that's the `_date' field.
2314 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2315 L<Time::Local> and L<Date::Parse> for conversion functions.
2317 I<template>, if specified, is the name of a suffix for alternate invoices.
2319 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2325 my $conf = $self->conf;
2326 my( $today, $template, %opt );
2328 %opt = %{ shift() };
2329 $today = delete($opt{'time'}) || '';
2330 $template = delete($opt{template}) || '';
2332 ( $today, $template, %opt ) = @_;
2335 my %params = ( 'format' => 'latex' );
2336 $params{'time'} = $today if $today;
2337 $params{'template'} = $template if $template;
2338 $params{$_} = $opt{$_}
2339 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2341 $template ||= $self->_agent_template;
2343 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2344 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2348 ) or die "can't open temp file: $!\n";
2350 my $agentnum = $self->cust_main->agentnum;
2352 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2353 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2354 or die "can't write temp file: $!\n";
2356 print $lh $conf->config_binary('logo.eps', $agentnum)
2357 or die "can't write temp file: $!\n";
2360 $params{'logo_file'} = $lh->filename;
2362 if($conf->exists('invoice-barcode')){
2363 my $png_file = $self->invoice_barcode($dir);
2364 my $eps_file = $png_file;
2365 $eps_file =~ s/\.png$/.eps/g;
2366 $png_file =~ /(barcode.*png)/;
2368 $eps_file =~ /(barcode.*eps)/;
2371 my $curr_dir = cwd();
2373 # after painfuly long experimentation, it was determined that sam2p won't
2374 # accept : and other chars in the path, no matter how hard I tried to
2375 # escape them, hence the chdir (and chdir back, just to be safe)
2376 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2377 or die "sam2p failed: $!\n";
2381 $params{'barcode_file'} = $eps_file;
2384 my @filled_in = $self->print_generic( %params );
2386 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2390 ) or die "can't open temp file: $!\n";
2391 binmode($fh, ':utf8'); # language support
2392 print $fh join('', @filled_in );
2395 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2396 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2400 =item invoice_barcode DIR_OR_FALSE
2402 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2403 it is taken as the temp directory where the PNG file will be generated and the
2404 PNG file name is returned. Otherwise, the PNG image itself is returned.
2408 sub invoice_barcode {
2409 my ($self, $dir) = (shift,shift);
2411 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2412 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2413 my $gd = $gdbar->plot(Height => 30);
2416 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2420 ) or die "can't open temp file: $!\n";
2421 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2422 my $png_file = $bh->filename;
2429 =item print_generic OPTION => VALUE ...
2431 Internal method - returns a filled-in template for this invoice as a scalar.
2433 See print_ps and print_pdf for methods that return PostScript and PDF output.
2435 Non optional options include
2436 format - latex, html, template
2438 Optional options include
2440 template - a value used as a suffix for a configuration template
2442 time - a value used to control the printing of overdue messages. The
2443 default is now. It isn't the date of the invoice; that's the `_date' field.
2444 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2445 L<Time::Local> and L<Date::Parse> for conversion functions.
2449 unsquelch_cdr - overrides any per customer cdr squelching when true
2451 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2453 locale - override customer's locale
2457 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2458 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2459 # yes: fixed width/plain text printing will be borked
2461 my( $self, %params ) = @_;
2462 my $conf = $self->conf;
2463 my $today = $params{today} ? $params{today} : time;
2464 warn "$me print_generic called on $self with suffix $params{template}\n"
2467 my $format = $params{format};
2468 die "Unknown format: $format"
2469 unless $format =~ /^(latex|html|template)$/;
2471 my $cust_main = $self->cust_main;
2472 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2473 unless $cust_main->payname
2474 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2476 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2477 'html' => [ '<%=', '%>' ],
2478 'template' => [ '{', '}' ],
2481 warn "$me print_generic creating template\n"
2484 #create the template
2485 my $template = $params{template} ? $params{template} : $self->_agent_template;
2486 my $templatefile = "invoice_$format";
2487 $templatefile .= "_$template"
2488 if length($template) && $conf->exists($templatefile."_$template");
2489 my @invoice_template = map "$_\n", $conf->config($templatefile)
2490 or die "cannot load config data $templatefile";
2493 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2494 #change this to a die when the old code is removed
2495 warn "old-style invoice template $templatefile; ".
2496 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2497 $old_latex = 'true';
2498 @invoice_template = _translate_old_latex_format(@invoice_template);
2501 warn "$me print_generic creating T:T object\n"
2504 my $text_template = new Text::Template(
2506 SOURCE => \@invoice_template,
2507 DELIMITERS => $delimiters{$format},
2510 warn "$me print_generic compiling T:T object\n"
2513 $text_template->compile()
2514 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2517 # additional substitution could possibly cause breakage in existing templates
2518 my %convert_maps = (
2520 'notes' => sub { map "$_", @_ },
2521 'footer' => sub { map "$_", @_ },
2522 'smallfooter' => sub { map "$_", @_ },
2523 'returnaddress' => sub { map "$_", @_ },
2524 'coupon' => sub { map "$_", @_ },
2525 'summary' => sub { map "$_", @_ },
2531 s/%%(.*)$/<!-- $1 -->/g;
2532 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2533 s/\\begin\{enumerate\}/<ol>/g;
2535 s/\\end\{enumerate\}/<\/ol>/g;
2536 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2545 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2547 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2552 s/\\\\\*?\s*$/<BR>/;
2553 s/\\hyphenation\{[\w\s\-]+}//;
2558 'coupon' => sub { "" },
2559 'summary' => sub { "" },
2566 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2567 s/\\begin\{enumerate\}//g;
2569 s/\\end\{enumerate\}//g;
2570 s/\\textbf\{(.*)\}/$1/g;
2577 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2579 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2584 s/\\\\\*?\s*$/\n/; # dubious
2585 s/\\hyphenation\{[\w\s\-]+}//;
2589 'coupon' => sub { "" },
2590 'summary' => sub { "" },
2595 # hashes for differing output formats
2596 my %nbsps = ( 'latex' => '~',
2597 'html' => '', # '&nbps;' would be nice
2598 'template' => '', # not used
2600 my $nbsp = $nbsps{$format};
2602 my %escape_functions = ( 'latex' => \&_latex_escape,
2603 'html' => \&_html_escape_nbsp,#\&encode_entities,
2604 'template' => sub { shift },
2606 my $escape_function = $escape_functions{$format};
2607 my $escape_function_nonbsp = ($format eq 'html')
2608 ? \&_html_escape : $escape_function;
2610 my %date_formats = ( 'latex' => $date_format_long,
2611 'html' => $date_format_long,
2614 $date_formats{'html'} =~ s/ / /g;
2616 my $date_format = $date_formats{$format};
2618 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2620 'html' => sub { return '<b>'. shift(). '</b>'
2622 'template' => sub { shift },
2624 my $embolden_function = $embolden_functions{$format};
2626 my %newline_tokens = ( 'latex' => '\\\\',
2630 my $newline_token = $newline_tokens{$format};
2632 warn "$me generating template variables\n"
2635 # generate template variables
2638 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2642 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2648 $returnaddress = join("\n",
2649 $conf->config_orbase("invoice_${format}returnaddress", $template)
2652 } elsif ( grep /\S/,
2653 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2655 my $convert_map = $convert_maps{$format}{'returnaddress'};
2658 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2663 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2665 my $convert_map = $convert_maps{$format}{'returnaddress'};
2666 $returnaddress = join( "\n", &$convert_map(
2667 map { s/( {2,})/'~' x length($1)/eg;
2671 ( $conf->config('company_name', $self->cust_main->agentnum),
2672 $conf->config('company_address', $self->cust_main->agentnum),
2679 my $warning = "Couldn't find a return address; ".
2680 "do you need to set the company_address configuration value?";
2682 $returnaddress = $nbsp;
2683 #$returnaddress = $warning;
2687 warn "$me generating invoice data\n"
2690 my $agentnum = $self->cust_main->agentnum;
2692 my %invoice_data = (
2695 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2696 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2697 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2698 'returnaddress' => $returnaddress,
2699 'agent' => &$escape_function($cust_main->agent->agent),
2702 'invnum' => $self->invnum,
2703 'date' => time2str($date_format, $self->_date),
2704 'today' => time2str($date_format_long, $today),
2705 'terms' => $self->terms,
2706 'template' => $template, #params{'template'},
2707 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2708 'current_charges' => sprintf("%.2f", $self->charged),
2709 'duedate' => $self->due_date2str($rdate_format), #date_format?
2712 'custnum' => $cust_main->display_custnum,
2713 'agent_custid' => &$escape_function($cust_main->agent_custid),
2714 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2715 payname company address1 address2 city state zip fax
2719 'ship_enable' => $conf->exists('invoice-ship_address'),
2720 'unitprices' => $conf->exists('invoice-unitprice'),
2721 'smallernotes' => $conf->exists('invoice-smallernotes'),
2722 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2723 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2725 #layout info -- would be fancy to calc some of this and bury the template
2727 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2728 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2729 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2730 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2731 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2732 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2733 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2734 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2735 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2736 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2738 # better hang on to conf_dir for a while (for old templates)
2739 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2741 #these are only used when doing paged plaintext
2748 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2749 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2750 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2751 # eval to avoid death for unimplemented languages
2752 my $dh = eval { Date::Language->new($info{'name'}) } ||
2753 Date::Language->new(); # fall back to English
2754 # prototype here to silence warnings
2755 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2756 # eventually use this date handle everywhere in here, too
2758 my $min_sdate = 999999999999;
2760 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2761 next unless $cust_bill_pkg->pkgnum > 0;
2762 $min_sdate = $cust_bill_pkg->sdate
2763 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2764 $max_edate = $cust_bill_pkg->edate
2765 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2768 $invoice_data{'bill_period'} = '';
2769 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2770 . " to " . time2str('%e %h', $max_edate)
2771 if ($max_edate != 0 && $min_sdate != 999999999999);
2773 $invoice_data{finance_section} = '';
2774 if ( $conf->config('finance_pkgclass') ) {
2776 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2777 $invoice_data{finance_section} = $pkg_class->categoryname;
2779 $invoice_data{finance_amount} = '0.00';
2780 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2782 my $countrydefault = $conf->config('countrydefault') || 'US';
2783 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2784 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2785 my $method = $prefix.$_;
2786 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2788 $invoice_data{'ship_country'} = ''
2789 if ( $invoice_data{'ship_country'} eq $countrydefault );
2791 $invoice_data{'cid'} = $params{'cid'}
2794 if ( $cust_main->country eq $countrydefault ) {
2795 $invoice_data{'country'} = '';
2797 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2801 $invoice_data{'address'} = \@address;
2803 $cust_main->payname.
2804 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2805 ? " (P.O. #". $cust_main->payinfo. ")"
2809 push @address, $cust_main->company
2810 if $cust_main->company;
2811 push @address, $cust_main->address1;
2812 push @address, $cust_main->address2
2813 if $cust_main->address2;
2815 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2816 push @address, $invoice_data{'country'}
2817 if $invoice_data{'country'};
2819 while (scalar(@address) < 5);
2821 $invoice_data{'logo_file'} = $params{'logo_file'}
2822 if $params{'logo_file'};
2823 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2824 if $params{'barcode_file'};
2825 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2826 if $params{'barcode_img'};
2827 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2828 if $params{'barcode_cid'};
2830 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2831 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2832 #my $balance_due = $self->owed + $pr_total - $cr_total;
2833 my $balance_due = $self->owed + $pr_total;
2835 # the customer's current balance as shown on the invoice before this one
2836 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2838 # the change in balance from that invoice to this one
2839 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2841 # the sum of amount owed on all previous invoices
2842 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2844 # the sum of amount owed on all invoices
2845 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2847 # info from customer's last invoice before this one, for some
2849 $invoice_data{'last_bill'} = {};
2850 my $last_bill = $pr_cust_bill[-1];
2852 $invoice_data{'last_bill'} = {
2853 '_date' => $last_bill->_date, #unformatted
2854 # all we need for now
2858 my $summarypage = '';
2859 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2862 $invoice_data{'summarypage'} = $summarypage;
2864 warn "$me substituting variables in notes, footer, smallfooter\n"
2867 my @include = (qw( notes footer smallfooter ));
2868 push @include, 'coupon' unless $params{'no_coupon'};
2869 foreach my $include (@include) {
2871 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2874 if ( $conf->exists($inc_file, $agentnum)
2875 && length( $conf->config($inc_file, $agentnum) ) ) {
2877 @inc_src = $conf->config($inc_file, $agentnum);
2881 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2883 my $convert_map = $convert_maps{$format}{$include};
2885 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2886 s/--\@\]/$delimiters{$format}[1]/g;
2889 &$convert_map( $conf->config($inc_file, $agentnum) );
2893 my $inc_tt = new Text::Template (
2895 SOURCE => [ map "$_\n", @inc_src ],
2896 DELIMITERS => $delimiters{$format},
2897 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2899 unless ( $inc_tt->compile() ) {
2900 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2901 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2905 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2907 $invoice_data{$include} =~ s/\n+$//
2908 if ($format eq 'latex');
2911 # let invoices use either of these as needed
2912 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2913 ? $cust_main->payinfo : '';
2914 $invoice_data{'po_line'} =
2915 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2916 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2919 my %money_chars = ( 'latex' => '',
2920 'html' => $conf->config('money_char') || '$',
2923 my $money_char = $money_chars{$format};
2925 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2926 'html' => $conf->config('money_char') || '$',
2929 my $other_money_char = $other_money_chars{$format};
2930 $invoice_data{'dollar'} = $other_money_char;
2932 my @detail_items = ();
2933 my @total_items = ();
2937 $invoice_data{'detail_items'} = \@detail_items;
2938 $invoice_data{'total_items'} = \@total_items;
2939 $invoice_data{'buf'} = \@buf;
2940 $invoice_data{'sections'} = \@sections;
2942 warn "$me generating sections\n"
2945 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2946 'subtotal' => $other_money_char.
2947 sprintf('%.2f', $pr_total),
2948 'summarized' => '', #why? $summarypage ? 'Y' : '',
2950 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2951 join(' / ', map { $cust_main->balance_date_range(@$_) }
2952 $self->_prior_month30s
2954 if $conf->exists('invoice_include_aging');
2957 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2958 'subtotal' => $taxtotal, # adjusted below
2960 my $tax_weight = _pkg_category($tax_section->{description})
2961 ? _pkg_category($tax_section->{description})->weight
2963 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2964 $tax_section->{'sort_weight'} = $tax_weight;
2967 my $adjusttotal = 0;
2968 my $adjust_section = { 'description' =>
2969 $self->mt('Credits, Payments, and Adjustments'),
2970 'subtotal' => 0, # adjusted below
2972 my $adjust_weight = _pkg_category($adjust_section->{description})
2973 ? _pkg_category($adjust_section->{description})->weight
2975 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2976 $adjust_section->{'sort_weight'} = $adjust_weight;
2978 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2979 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2980 $invoice_data{'multisection'} = $multisection;
2981 my $late_sections = [];
2982 my $extra_sections = [];
2983 my $extra_lines = ();
2985 my $default_section = { 'description' => '',
2990 if ( $multisection ) {
2991 ($extra_sections, $extra_lines) =
2992 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2993 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2995 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2997 push @detail_items, @$extra_lines if $extra_lines;
2999 $self->_items_sections( $late_sections, # this could stand a refactor
3001 $escape_function_nonbsp,
3005 if ($conf->exists('svc_phone_sections')) {
3006 my ($phone_sections, $phone_lines) =
3007 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3008 push @{$late_sections}, @$phone_sections;
3009 push @detail_items, @$phone_lines;
3011 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3012 my ($accountcode_section, $accountcode_lines) =
3013 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3014 if ( scalar(@$accountcode_lines) ) {
3015 push @{$late_sections}, $accountcode_section;
3016 push @detail_items, @$accountcode_lines;
3019 } else {# not multisection
3020 # make a default section
3021 push @sections, $default_section;
3022 # and calculate the finance charge total, since it won't get done otherwise.
3023 # XXX possibly other totals?
3024 # XXX possibly finance_pkgclass should not be used in this manner?
3025 if ( $conf->exists('finance_pkgclass') ) {
3026 my @finance_charges;
3027 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3028 if ( grep { $_->section eq $invoice_data{finance_section} }
3029 $cust_bill_pkg->cust_bill_pkg_display ) {
3030 # I think these are always setup fees, but just to be sure...
3031 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3034 $invoice_data{finance_amount} =
3035 sprintf('%.2f', sum( @finance_charges ) || 0);
3039 unless ( $conf->exists('disable_previous_balance', $agentnum)
3040 || $conf->exists('previous_balance-summary_only')
3044 warn "$me adding previous balances\n"
3047 foreach my $line_item ( $self->_items_previous ) {
3050 ext_description => [],
3052 $detail->{'ref'} = $line_item->{'pkgnum'};
3053 $detail->{'quantity'} = 1;
3054 $detail->{'section'} = $multisection ? $previous_section
3056 $detail->{'description'} = &$escape_function($line_item->{'description'});
3057 if ( exists $line_item->{'ext_description'} ) {
3058 @{$detail->{'ext_description'}} = map {
3059 &$escape_function($_);
3060 } @{$line_item->{'ext_description'}};
3062 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3063 $line_item->{'amount'};
3064 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3066 push @detail_items, $detail;
3067 push @buf, [ $detail->{'description'},
3068 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3074 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) )
3076 push @buf, ['','-----------'];
3077 push @buf, [ $self->mt('Total Previous Balance'),
3078 $money_char. sprintf("%10.2f", $pr_total) ];
3082 if ( $conf->exists('svc_phone-did-summary') ) {
3083 warn "$me adding DID summary\n"
3086 my ($didsummary,$minutes) = $self->_did_summary;
3087 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3089 { 'description' => $didsummary_desc,
3090 'ext_description' => [ $didsummary, $minutes ],
3094 foreach my $section (@sections, @$late_sections) {
3096 warn "$me adding section \n". Dumper($section)
3099 # begin some normalization
3100 $section->{'subtotal'} = $section->{'amount'}
3102 && !exists($section->{subtotal})
3103 && exists($section->{amount});
3105 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3106 if ( $invoice_data{finance_section} &&
3107 $section->{'description'} eq $invoice_data{finance_section} );
3109 $section->{'subtotal'} = $other_money_char.
3110 sprintf('%.2f', $section->{'subtotal'})
3113 # continue some normalization
3114 $section->{'amount'} = $section->{'subtotal'}
3118 if ( $section->{'description'} ) {
3119 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3124 warn "$me setting options\n"
3127 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3129 $options{'section'} = $section if $multisection;
3130 $options{'format'} = $format;
3131 $options{'escape_function'} = $escape_function;
3132 $options{'no_usage'} = 1 unless $unsquelched;
3133 $options{'unsquelched'} = $unsquelched;
3134 $options{'summary_page'} = $summarypage;
3135 $options{'skip_usage'} =
3136 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3137 $options{'multilocation'} = $multilocation;
3138 $options{'multisection'} = $multisection;
3140 warn "$me searching for line items\n"
3143 foreach my $line_item ( $self->_items_pkg(%options) ) {
3145 warn "$me adding line item $line_item\n"
3149 ext_description => [],
3151 $detail->{'ref'} = $line_item->{'pkgnum'};
3152 $detail->{'quantity'} = $line_item->{'quantity'};
3153 $detail->{'section'} = $section;
3154 $detail->{'description'} = &$escape_function($line_item->{'description'});
3155 if ( exists $line_item->{'ext_description'} ) {
3156 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3158 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3159 $line_item->{'amount'};
3160 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3161 $line_item->{'unit_amount'};
3162 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3164 $detail->{'sdate'} = $line_item->{'sdate'};
3165 $detail->{'edate'} = $line_item->{'edate'};
3166 $detail->{'seconds'} = $line_item->{'seconds'};
3168 push @detail_items, $detail;
3169 push @buf, ( [ $detail->{'description'},
3170 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3172 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3176 if ( $section->{'description'} ) {
3177 push @buf, ( ['','-----------'],
3178 [ $section->{'description'}. ' sub-total',
3179 $section->{'subtotal'} # already formatted this
3188 $invoice_data{current_less_finance} =
3189 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3191 if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
3192 || $conf->exists('previous_balance-summary_only') )
3194 unshift @sections, $previous_section if $pr_total;
3197 warn "$me adding taxes\n"
3200 foreach my $tax ( $self->_items_tax ) {
3202 $taxtotal += $tax->{'amount'};
3204 my $description = &$escape_function( $tax->{'description'} );
3205 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3207 if ( $multisection ) {
3209 my $money = $old_latex ? '' : $money_char;
3210 push @detail_items, {
3211 ext_description => [],
3214 description => $description,
3215 amount => $money. $amount,
3217 section => $tax_section,
3222 push @total_items, {
3223 'total_item' => $description,
3224 'total_amount' => $other_money_char. $amount,
3229 push @buf,[ $description,
3230 $money_char. $amount,
3237 $total->{'total_item'} = $self->mt('Sub-total');
3238 $total->{'total_amount'} =
3239 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3241 if ( $multisection ) {
3242 $tax_section->{'subtotal'} = $other_money_char.
3243 sprintf('%.2f', $taxtotal);
3244 $tax_section->{'pretotal'} = 'New charges sub-total '.
3245 $total->{'total_amount'};
3246 push @sections, $tax_section if $taxtotal;
3248 unshift @total_items, $total;
3251 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3253 push @buf,['','-----------'];
3254 push @buf,[$self->mt(
3255 $conf->exists('disable_previous_balance', $agentnum)
3257 : 'Total New Charges'
3259 $money_char. sprintf("%10.2f",$self->charged) ];
3265 $item = $conf->config('previous_balance-exclude_from_total')
3266 || 'Total New Charges'
3267 if $conf->exists('previous_balance-exclude_from_total');
3268 my $amount = $self->charged +
3269 ( $conf->exists('disable_previous_balance', $agentnum) ||
3270 $conf->exists('previous_balance-exclude_from_total')
3274 $total->{'total_item'} = &$embolden_function($self->mt($item));
3275 $total->{'total_amount'} =
3276 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3277 if ( $multisection ) {
3278 if ( $adjust_section->{'sort_weight'} ) {
3279 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3280 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3282 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3283 $other_money_char. sprintf('%.2f', $self->charged );
3286 push @total_items, $total;
3288 push @buf,['','-----------'];
3291 sprintf( '%10.2f', $amount )
3296 unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
3297 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3300 my $credittotal = 0;
3301 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3304 $total->{'total_item'} = &$escape_function($credit->{'description'});
3305 $credittotal += $credit->{'amount'};
3306 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3307 $adjusttotal += $credit->{'amount'};
3308 if ( $multisection ) {
3309 my $money = $old_latex ? '' : $money_char;
3310 push @detail_items, {
3311 ext_description => [],
3314 description => &$escape_function($credit->{'description'}),
3315 amount => $money. $credit->{'amount'},
3317 section => $adjust_section,
3320 push @total_items, $total;
3324 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3327 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3328 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3332 my $paymenttotal = 0;
3333 foreach my $payment ( $self->_items_payments ) {
3335 $total->{'total_item'} = &$escape_function($payment->{'description'});
3336 $paymenttotal += $payment->{'amount'};
3337 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3338 $adjusttotal += $payment->{'amount'};
3339 if ( $multisection ) {
3340 my $money = $old_latex ? '' : $money_char;
3341 push @detail_items, {
3342 ext_description => [],
3345 description => &$escape_function($payment->{'description'}),
3346 amount => $money. $payment->{'amount'},
3348 section => $adjust_section,
3351 push @total_items, $total;
3353 push @buf, [ $payment->{'description'},
3354 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3357 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3359 if ( $multisection ) {
3360 $adjust_section->{'subtotal'} = $other_money_char.
3361 sprintf('%.2f', $adjusttotal);
3362 push @sections, $adjust_section
3363 unless $adjust_section->{sort_weight};
3366 # create Balance Due message
3369 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3370 $total->{'total_amount'} =
3371 &$embolden_function(
3372 $other_money_char. sprintf('%.2f', $summarypage
3374 $self->billing_balance
3375 : $self->owed + $pr_total
3378 if ( $multisection && !$adjust_section->{sort_weight} ) {
3379 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3380 $total->{'total_amount'};
3382 push @total_items, $total;
3384 push @buf,['','-----------'];
3385 push @buf,[$self->balance_due_msg, $money_char.
3386 sprintf("%10.2f", $balance_due ) ];
3389 if ( $conf->exists('previous_balance-show_credit')
3390 and $cust_main->balance < 0 ) {
3391 my $credit_total = {
3392 'total_item' => &$embolden_function($self->credit_balance_msg),
3393 'total_amount' => &$embolden_function(
3394 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3397 if ( $multisection ) {
3398 $adjust_section->{'posttotal'} .= $newline_token .
3399 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3402 push @total_items, $credit_total;
3404 push @buf,['','-----------'];
3405 push @buf,[$self->credit_balance_msg, $money_char.
3406 sprintf("%10.2f", -$cust_main->balance ) ];
3410 if ( $multisection ) {
3411 if ($conf->exists('svc_phone_sections')) {
3413 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3414 $total->{'total_amount'} =
3415 &$embolden_function(
3416 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3418 my $last_section = pop @sections;
3419 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3420 $total->{'total_amount'};
3421 push @sections, $last_section;
3423 push @sections, @$late_sections
3427 # make a discounts-available section, even without multisection
3428 if ( $conf->exists('discount-show_available')
3429 and my @discounts_avail = $self->_items_discounts_avail ) {
3430 my $discount_section = {
3431 'description' => $self->mt('Discounts Available'),
3436 push @sections, $discount_section;
3437 push @detail_items, map { +{
3438 'ref' => '', #should this be something else?
3439 'section' => $discount_section,
3440 'description' => &$escape_function( $_->{description} ),
3441 'amount' => $money_char . &$escape_function( $_->{amount} ),
3442 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3443 } } @discounts_avail;
3446 # All sections and items are built; now fill in templates.
3447 my @includelist = ();
3448 push @includelist, 'summary' if $summarypage;
3449 foreach my $include ( @includelist ) {
3451 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3454 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3456 @inc_src = $conf->config($inc_file, $agentnum);
3460 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3462 my $convert_map = $convert_maps{$format}{$include};
3464 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3465 s/--\@\]/$delimiters{$format}[1]/g;
3468 &$convert_map( $conf->config($inc_file, $agentnum) );
3472 my $inc_tt = new Text::Template (
3474 SOURCE => [ map "$_\n", @inc_src ],
3475 DELIMITERS => $delimiters{$format},
3476 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3478 unless ( $inc_tt->compile() ) {
3479 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3480 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3484 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3486 $invoice_data{$include} =~ s/\n+$//
3487 if ($format eq 'latex');
3492 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3493 /invoice_lines\((\d*)\)/;
3494 $invoice_lines += $1 || scalar(@buf);
3497 die "no invoice_lines() functions in template?"
3498 if ( $format eq 'template' && !$wasfunc );
3500 if ($format eq 'template') {
3502 if ( $invoice_lines ) {
3503 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3504 $invoice_data{'total_pages'}++
3505 if scalar(@buf) % $invoice_lines;
3508 #setup subroutine for the template
3509 $invoice_data{invoice_lines} = sub {
3510 my $lines = shift || scalar(@buf);
3522 push @collect, split("\n",
3523 $text_template->fill_in( HASH => \%invoice_data )
3525 $invoice_data{'page'}++;
3527 map "$_\n", @collect;
3529 # this is where we actually create the invoice
3530 warn "filling in template for invoice ". $self->invnum. "\n"
3532 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3535 $text_template->fill_in(HASH => \%invoice_data);
3539 # helper routine for generating date ranges
3540 sub _prior_month30s {
3543 [ 1, 2592000 ], # 0-30 days ago
3544 [ 2592000, 5184000 ], # 30-60 days ago
3545 [ 5184000, 7776000 ], # 60-90 days ago
3546 [ 7776000, 0 ], # 90+ days ago
3549 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3550 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3555 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3557 Returns an postscript invoice, as a scalar.
3559 Options can be passed as a hashref (recommended) or as a list of time, template
3560 and then any key/value pairs for any other options.
3562 I<time> an optional value used to control the printing of overdue messages. The
3563 default is now. It isn't the date of the invoice; that's the `_date' field.
3564 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3565 L<Time::Local> and L<Date::Parse> for conversion functions.
3567 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3574 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3575 my $ps = generate_ps($file);
3577 unlink($barcodefile) if $barcodefile;
3582 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3584 Returns an PDF invoice, as a scalar.
3586 Options can be passed as a hashref (recommended) or as a list of time, template
3587 and then any key/value pairs for any other options.
3589 I<time> an optional value used to control the printing of overdue messages. The
3590 default is now. It isn't the date of the invoice; that's the `_date' field.
3591 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3592 L<Time::Local> and L<Date::Parse> for conversion functions.
3594 I<template>, if specified, is the name of a suffix for alternate invoices.
3596 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3603 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3604 my $pdf = generate_pdf($file);
3606 unlink($barcodefile) if $barcodefile;
3611 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3613 Returns an HTML invoice, as a scalar.
3615 I<time> an optional value used to control the printing of overdue messages. The
3616 default is now. It isn't the date of the invoice; that's the `_date' field.
3617 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3618 L<Time::Local> and L<Date::Parse> for conversion functions.
3620 I<template>, if specified, is the name of a suffix for alternate invoices.
3622 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3624 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3625 when emailing the invoice as part of a multipart/related MIME email.
3633 %params = %{ shift() };
3635 $params{'time'} = shift;
3636 $params{'template'} = shift;
3637 $params{'cid'} = shift;
3640 $params{'format'} = 'html';
3642 $self->print_generic( %params );
3645 # quick subroutine for print_latex
3647 # There are ten characters that LaTeX treats as special characters, which
3648 # means that they do not simply typeset themselves:
3649 # # $ % & ~ _ ^ \ { }
3651 # TeX ignores blanks following an escaped character; if you want a blank (as
3652 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3656 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3657 $value =~ s/([<>])/\$$1\$/g;
3663 encode_entities($value);
3667 sub _html_escape_nbsp {
3668 my $value = _html_escape(shift);
3669 $value =~ s/ +/ /g;
3673 #utility methods for print_*
3675 sub _translate_old_latex_format {
3676 warn "_translate_old_latex_format called\n"
3683 if ( $line =~ /^%%Detail\s*$/ ) {
3685 push @template, q![@--!,
3686 q! foreach my $_tr_line (@detail_items) {!,
3687 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3688 q! $_tr_line->{'description'} .= !,
3689 q! "\\tabularnewline\n~~".!,
3690 q! join( "\\tabularnewline\n~~",!,
3691 q! @{$_tr_line->{'ext_description'}}!,
3695 while ( ( my $line_item_line = shift )
3696 !~ /^%%EndDetail\s*$/ ) {
3697 $line_item_line =~ s/'/\\'/g; # nice LTS
3698 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3699 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3700 push @template, " \$OUT .= '$line_item_line';";
3703 push @template, '}',
3706 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3708 push @template, '[@--',
3709 ' foreach my $_tr_line (@total_items) {';
3711 while ( ( my $total_item_line = shift )
3712 !~ /^%%EndTotalDetails\s*$/ ) {
3713 $total_item_line =~ s/'/\\'/g; # nice LTS
3714 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3715 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3716 push @template, " \$OUT .= '$total_item_line';";
3719 push @template, '}',
3723 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3724 push @template, $line;
3730 warn "$_\n" foreach @template;
3738 my $conf = $self->conf;
3740 #check for an invoice-specific override
3741 return $self->invoice_terms if $self->invoice_terms;
3743 #check for a customer- specific override
3744 my $cust_main = $self->cust_main;
3745 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3747 #use configured default
3748 $conf->config('invoice_default_terms') || '';
3754 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3755 $duedate = $self->_date() + ( $1 * 86400 );
3762 $self->due_date ? time2str(shift, $self->due_date) : '';
3765 sub balance_due_msg {
3767 my $msg = $self->mt('Balance Due');
3768 return $msg unless $self->terms;
3769 if ( $self->due_date ) {
3770 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3771 $self->due_date2str($date_format);
3772 } elsif ( $self->terms ) {
3773 $msg .= ' - '. $self->terms;
3778 sub balance_due_date {
3780 my $conf = $self->conf;
3782 if ( $conf->exists('invoice_default_terms')
3783 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3784 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3789 sub credit_balance_msg {
3791 $self->mt('Credit Balance Remaining')
3794 =item invnum_date_pretty
3796 Returns a string with the invoice number and date, for example:
3797 "Invoice #54 (3/20/2008)"
3801 sub invnum_date_pretty {
3803 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3808 Returns a string with the date, for example: "3/20/2008"
3814 time2str($date_format, $self->_date);
3817 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3819 Generate section information for all items appearing on this invoice.
3820 This will only be called for multi-section invoices.
3822 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3823 related display records (L<FS::cust_bill_pkg_display>) and organize
3824 them into two groups ("early" and "late" according to whether they come
3825 before or after the total), then into sections. A subtotal is calculated
3828 Section descriptions are returned in sort weight order. Each consists
3829 of a hash containing:
3831 description: the package category name, escaped
3832 subtotal: the total charges in that section
3833 tax_section: a flag indicating that the section contains only tax charges
3834 summarized: same as tax_section, for some reason
3835 sort_weight: the package category's sort weight
3837 If 'condense' is set on the display record, it also contains everything
3838 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3839 coderefs to generate parts of the invoice. This is not advised.
3843 LATE: an arrayref to push the "late" section hashes onto. The "early"
3844 group is simply returned from the method.
3846 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3847 Turning this on has the following effects:
3848 - Ignores display items with the 'summary' flag.
3849 - Combines all items into the "early" group.
3850 - Creates sections for all non-disabled package categories, even if they
3851 have no charges on this invoice, as well as a section with no name.
3853 ESCAPE: an escape function to use for section titles.
3855 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3856 sorted list. If there are any of these, section subtotals exclude
3859 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3860 passed through to C<_condense_section()>.
3864 use vars qw(%pkg_category_cache);
3865 sub _items_sections {
3868 my $summarypage = shift;
3870 my $extra_sections = shift;
3874 my %late_subtotal = ();
3877 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3880 my $usage = $cust_bill_pkg->usage;
3882 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3883 next if ( $display->summary && $summarypage );
3885 my $section = $display->section;
3886 my $type = $display->type;
3888 $not_tax{$section} = 1
3889 unless $cust_bill_pkg->pkgnum == 0;
3891 if ( $display->post_total && !$summarypage ) {
3892 if (! $type || $type eq 'S') {
3893 $late_subtotal{$section} += $cust_bill_pkg->setup
3894 if $cust_bill_pkg->setup != 0
3895 || $cust_bill_pkg->setup_show_zero;
3899 $late_subtotal{$section} += $cust_bill_pkg->recur
3900 if $cust_bill_pkg->recur != 0
3901 || $cust_bill_pkg->recur_show_zero;
3904 if ($type && $type eq 'R') {
3905 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3906 if $cust_bill_pkg->recur != 0
3907 || $cust_bill_pkg->recur_show_zero;
3910 if ($type && $type eq 'U') {
3911 $late_subtotal{$section} += $usage
3912 unless scalar(@$extra_sections);
3917 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3919 if (! $type || $type eq 'S') {
3920 $subtotal{$section} += $cust_bill_pkg->setup
3921 if $cust_bill_pkg->setup != 0
3922 || $cust_bill_pkg->setup_show_zero;
3926 $subtotal{$section} += $cust_bill_pkg->recur
3927 if $cust_bill_pkg->recur != 0
3928 || $cust_bill_pkg->recur_show_zero;
3931 if ($type && $type eq 'R') {
3932 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3933 if $cust_bill_pkg->recur != 0
3934 || $cust_bill_pkg->recur_show_zero;
3937 if ($type && $type eq 'U') {
3938 $subtotal{$section} += $usage
3939 unless scalar(@$extra_sections);
3948 %pkg_category_cache = ();
3950 push @$late, map { { 'description' => &{$escape}($_),
3951 'subtotal' => $late_subtotal{$_},
3953 'sort_weight' => ( _pkg_category($_)
3954 ? _pkg_category($_)->weight
3957 ((_pkg_category($_) && _pkg_category($_)->condense)
3958 ? $self->_condense_section($format)
3962 sort _sectionsort keys %late_subtotal;
3965 if ( $summarypage ) {
3966 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3967 map { $_->categoryname } qsearch('pkg_category', {});
3968 push @sections, '' if exists($subtotal{''});
3970 @sections = keys %subtotal;
3973 my @early = map { { 'description' => &{$escape}($_),
3974 'subtotal' => $subtotal{$_},
3975 'summarized' => $not_tax{$_} ? '' : 'Y',
3976 'tax_section' => $not_tax{$_} ? '' : 'Y',
3977 'sort_weight' => ( _pkg_category($_)
3978 ? _pkg_category($_)->weight
3981 ((_pkg_category($_) && _pkg_category($_)->condense)
3982 ? $self->_condense_section($format)
3987 push @early, @$extra_sections if $extra_sections;
3989 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3993 #helper subs for above
3996 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4000 my $categoryname = shift;
4001 $pkg_category_cache{$categoryname} ||=
4002 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4005 my %condensed_format = (
4006 'label' => [ qw( Description Qty Amount ) ],
4008 sub { shift->{description} },
4009 sub { shift->{quantity} },
4010 sub { my($href, %opt) = @_;
4011 ($opt{dollar} || ''). $href->{amount};
4014 'align' => [ qw( l r r ) ],
4015 'span' => [ qw( 5 1 1 ) ], # unitprices?
4016 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4019 sub _condense_section {
4020 my ( $self, $format ) = ( shift, shift );
4022 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4023 qw( description_generator
4026 total_line_generator
4031 sub _condensed_generator_defaults {
4032 my ( $self, $format ) = ( shift, shift );
4033 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4042 sub _condensed_header_generator {
4043 my ( $self, $format ) = ( shift, shift );
4045 my ( $f, $prefix, $suffix, $separator, $column ) =
4046 _condensed_generator_defaults($format);
4048 if ($format eq 'latex') {
4049 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4050 $suffix = "\\\\\n\\hline";
4053 sub { my ($d,$a,$s,$w) = @_;
4054 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4056 } elsif ( $format eq 'html' ) {
4057 $prefix = '<th></th>';
4061 sub { my ($d,$a,$s,$w) = @_;
4062 return qq!<th align="$html_align{$a}">$d</th>!;
4070 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4072 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4075 $prefix. join($separator, @result). $suffix;
4080 sub _condensed_description_generator {
4081 my ( $self, $format ) = ( shift, shift );
4083 my ( $f, $prefix, $suffix, $separator, $column ) =
4084 _condensed_generator_defaults($format);
4086 my $money_char = '$';
4087 if ($format eq 'latex') {
4088 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4090 $separator = " & \n";
4092 sub { my ($d,$a,$s,$w) = @_;
4093 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4095 $money_char = '\\dollar';
4096 }elsif ( $format eq 'html' ) {
4097 $prefix = '"><td align="center"></td>';
4101 sub { my ($d,$a,$s,$w) = @_;
4102 return qq!<td align="$html_align{$a}">$d</td>!;
4104 #$money_char = $conf->config('money_char') || '$';
4105 $money_char = ''; # this is madness
4113 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4115 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4117 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4118 map { $f->{$_}->[$i] } qw(align span width)
4122 $prefix. join( $separator, @result ). $suffix;
4127 sub _condensed_total_generator {
4128 my ( $self, $format ) = ( shift, shift );
4130 my ( $f, $prefix, $suffix, $separator, $column ) =
4131 _condensed_generator_defaults($format);
4134 if ($format eq 'latex') {
4137 $separator = " & \n";
4139 sub { my ($d,$a,$s,$w) = @_;
4140 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4142 }elsif ( $format eq 'html' ) {
4146 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4148 sub { my ($d,$a,$s,$w) = @_;
4149 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4158 # my $r = &{$f->{fields}->[$i]}(@args);
4159 # $r .= ' Total' unless $i;
4161 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4163 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4164 map { $f->{$_}->[$i] } qw(align span width)
4168 $prefix. join( $separator, @result ). $suffix;
4173 =item total_line_generator FORMAT
4175 Returns a coderef used for generation of invoice total line items for this
4176 usage_class. FORMAT is either html or latex
4180 # should not be used: will have issues with hash element names (description vs
4181 # total_item and amount vs total_amount -- another array of functions?
4183 sub _condensed_total_line_generator {
4184 my ( $self, $format ) = ( shift, shift );
4186 my ( $f, $prefix, $suffix, $separator, $column ) =
4187 _condensed_generator_defaults($format);
4190 if ($format eq 'latex') {
4193 $separator = " & \n";
4195 sub { my ($d,$a,$s,$w) = @_;
4196 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4198 }elsif ( $format eq 'html' ) {
4202 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4204 sub { my ($d,$a,$s,$w) = @_;
4205 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4214 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4216 &{$column}( &{$f->{fields}->[$i]}(@args),
4217 map { $f->{$_}->[$i] } qw(align span width)
4221 $prefix. join( $separator, @result ). $suffix;
4226 #sub _items_extra_usage_sections {
4228 # my $escape = shift;
4230 # my %sections = ();
4232 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4233 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4235 # next unless $cust_bill_pkg->pkgnum > 0;
4237 # foreach my $section ( keys %usage_class ) {
4239 # my $usage = $cust_bill_pkg->usage($section);
4241 # next unless $usage && $usage > 0;
4243 # $sections{$section} ||= 0;
4244 # $sections{$section} += $usage;
4250 # map { { 'description' => &{$escape}($_),
4251 # 'subtotal' => $sections{$_},
4252 # 'summarized' => '',
4253 # 'tax_section' => '',
4256 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4260 sub _items_extra_usage_sections {
4262 my $conf = $self->conf;
4270 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4272 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4273 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4274 next unless $cust_bill_pkg->pkgnum > 0;
4276 foreach my $classnum ( keys %usage_class ) {
4277 my $section = $usage_class{$classnum}->classname;
4278 $classnums{$section} = $classnum;
4280 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4281 my $amount = $detail->amount;
4282 next unless $amount && $amount > 0;
4284 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4285 $sections{$section}{amount} += $amount; #subtotal
4286 $sections{$section}{calls}++;
4287 $sections{$section}{duration} += $detail->duration;
4289 my $desc = $detail->regionname;
4290 my $description = $desc;
4291 $description = substr($desc, 0, $maxlength). '...'
4292 if $format eq 'latex' && length($desc) > $maxlength;
4294 $lines{$section}{$desc} ||= {
4295 description => &{$escape}($description),
4296 #pkgpart => $part_pkg->pkgpart,
4297 pkgnum => $cust_bill_pkg->pkgnum,
4302 #unit_amount => $cust_bill_pkg->unitrecur,
4303 quantity => $cust_bill_pkg->quantity,
4304 product_code => 'N/A',
4305 ext_description => [],
4308 $lines{$section}{$desc}{amount} += $amount;
4309 $lines{$section}{$desc}{calls}++;
4310 $lines{$section}{$desc}{duration} += $detail->duration;
4316 my %sectionmap = ();
4317 foreach (keys %sections) {
4318 my $usage_class = $usage_class{$classnums{$_}};
4319 $sectionmap{$_} = { 'description' => &{$escape}($_),
4320 'amount' => $sections{$_}{amount}, #subtotal
4321 'calls' => $sections{$_}{calls},
4322 'duration' => $sections{$_}{duration},
4324 'tax_section' => '',
4325 'sort_weight' => $usage_class->weight,
4326 ( $usage_class->format
4327 ? ( map { $_ => $usage_class->$_($format) }
4328 qw( description_generator header_generator total_generator total_line_generator )
4335 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4339 foreach my $section ( keys %lines ) {
4340 foreach my $line ( keys %{$lines{$section}} ) {
4341 my $l = $lines{$section}{$line};
4342 $l->{section} = $sectionmap{$section};
4343 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4344 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4349 return(\@sections, \@lines);
4355 my $end = $self->_date;
4357 # start at date of previous invoice + 1 second or 0 if no previous invoice
4358 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4359 $start = 0 if !$start;
4362 my $cust_main = $self->cust_main;
4363 my @pkgs = $cust_main->all_pkgs;
4364 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4367 foreach my $pkg ( @pkgs ) {
4368 my @h_cust_svc = $pkg->h_cust_svc($end);
4369 foreach my $h_cust_svc ( @h_cust_svc ) {
4370 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4371 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4373 my $inserted = $h_cust_svc->date_inserted;
4374 my $deleted = $h_cust_svc->date_deleted;
4375 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4377 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4379 # DID either activated or ported in; cannot be both for same DID simultaneously
4380 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4381 && (!$phone_inserted->lnp_status
4382 || $phone_inserted->lnp_status eq ''
4383 || $phone_inserted->lnp_status eq 'native')) {
4386 else { # this one not so clean, should probably move to (h_)svc_phone
4387 my $phone_portedin = qsearchs( 'h_svc_phone',
4388 { 'svcnum' => $h_cust_svc->svcnum,
4389 'lnp_status' => 'portedin' },
4390 FS::h_svc_phone->sql_h_searchs($end),
4392 $num_portedin++ if $phone_portedin;
4395 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4396 if($deleted >= $start && $deleted <= $end && $phone_deleted
4397 && (!$phone_deleted->lnp_status
4398 || $phone_deleted->lnp_status ne 'portingout')) {
4401 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4402 && $phone_deleted->lnp_status
4403 && $phone_deleted->lnp_status eq 'portingout') {
4407 # increment usage minutes
4408 if ( $phone_inserted ) {
4409 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4410 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4413 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4416 # don't look at this service again
4417 push @seen, $h_cust_svc->svcnum;
4421 $minutes = sprintf("%d", $minutes);
4422 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4423 . "$num_deactivated Ported-Out: $num_portedout ",
4424 "Total Minutes: $minutes");
4427 sub _items_accountcode_cdr {
4432 my $section = { 'amount' => 0,
4435 'sort_weight' => '',
4437 'description' => 'Usage by Account Code',
4443 my %accountcodes = ();
4445 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4446 next unless $cust_bill_pkg->pkgnum > 0;
4448 my @header = $cust_bill_pkg->details_header;
4449 next unless scalar(@header);
4450 $section->{'header'} = join(',',@header);
4452 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4454 $section->{'header'} = $detail->formatted('format' => $format)
4455 if($detail->detail eq $section->{'header'});
4457 my $accountcode = $detail->accountcode;
4458 next unless $accountcode;
4460 my $amount = $detail->amount;
4461 next unless $amount && $amount > 0;
4463 $accountcodes{$accountcode} ||= {
4464 description => $accountcode,
4471 product_code => 'N/A',
4472 section => $section,
4473 ext_description => [ $section->{'header'} ],
4477 $section->{'amount'} += $amount;
4478 $accountcodes{$accountcode}{'amount'} += $amount;
4479 $accountcodes{$accountcode}{calls}++;
4480 $accountcodes{$accountcode}{duration} += $detail->duration;
4481 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4485 foreach my $l ( values %accountcodes ) {
4486 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4487 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4488 foreach my $sorted_detail ( @sorted_detail ) {
4489 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4491 delete $l->{detail_temp};
4495 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4497 return ($section,\@sorted_lines);
4500 sub _items_svc_phone_sections {
4502 my $conf = $self->conf;
4510 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4512 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4513 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4515 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4516 next unless $cust_bill_pkg->pkgnum > 0;
4518 my @header = $cust_bill_pkg->details_header;
4519 next unless scalar(@header);
4521 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4523 my $phonenum = $detail->phonenum;
4524 next unless $phonenum;
4526 my $amount = $detail->amount;
4527 next unless $amount && $amount > 0;
4529 $sections{$phonenum} ||= { 'amount' => 0,
4532 'sort_weight' => -1,
4533 'phonenum' => $phonenum,
4535 $sections{$phonenum}{amount} += $amount; #subtotal
4536 $sections{$phonenum}{calls}++;
4537 $sections{$phonenum}{duration} += $detail->duration;
4539 my $desc = $detail->regionname;
4540 my $description = $desc;
4541 $description = substr($desc, 0, $maxlength). '...'
4542 if $format eq 'latex' && length($desc) > $maxlength;
4544 $lines{$phonenum}{$desc} ||= {
4545 description => &{$escape}($description),
4546 #pkgpart => $part_pkg->pkgpart,
4554 product_code => 'N/A',
4555 ext_description => [],
4558 $lines{$phonenum}{$desc}{amount} += $amount;
4559 $lines{$phonenum}{$desc}{calls}++;
4560 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4562 my $line = $usage_class{$detail->classnum}->classname;
4563 $sections{"$phonenum $line"} ||=
4567 'sort_weight' => $usage_class{$detail->classnum}->weight,
4568 'phonenum' => $phonenum,
4569 'header' => [ @header ],
4571 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4572 $sections{"$phonenum $line"}{calls}++;
4573 $sections{"$phonenum $line"}{duration} += $detail->duration;
4575 $lines{"$phonenum $line"}{$desc} ||= {
4576 description => &{$escape}($description),
4577 #pkgpart => $part_pkg->pkgpart,
4585 product_code => 'N/A',
4586 ext_description => [],
4589 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4590 $lines{"$phonenum $line"}{$desc}{calls}++;
4591 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4592 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4593 $detail->formatted('format' => $format);
4598 my %sectionmap = ();
4599 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4600 foreach ( keys %sections ) {
4601 my @header = @{ $sections{$_}{header} || [] };
4603 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4604 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4605 my $usage_class = $summary ? $simple : $usage_simple;
4606 my $ending = $summary ? ' usage charges' : '';
4609 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4611 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4612 'amount' => $sections{$_}{amount}, #subtotal
4613 'calls' => $sections{$_}{calls},
4614 'duration' => $sections{$_}{duration},
4616 'tax_section' => '',
4617 'phonenum' => $sections{$_}{phonenum},
4618 'sort_weight' => $sections{$_}{sort_weight},
4619 'post_total' => $summary, #inspire pagebreak
4621 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4622 qw( description_generator
4625 total_line_generator
4632 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4633 $a->{sort_weight} <=> $b->{sort_weight}
4638 foreach my $section ( keys %lines ) {
4639 foreach my $line ( keys %{$lines{$section}} ) {
4640 my $l = $lines{$section}{$line};
4641 $l->{section} = $sectionmap{$section};
4642 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4643 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4648 if($conf->exists('phone_usage_class_summary')) {
4649 # this only works with Latex
4653 # after this, we'll have only two sections per DID:
4654 # Calls Summary and Calls Detail
4655 foreach my $section ( @sections ) {
4656 if($section->{'post_total'}) {
4657 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4658 $section->{'total_line_generator'} = sub { '' };
4659 $section->{'total_generator'} = sub { '' };
4660 $section->{'header_generator'} = sub { '' };
4661 $section->{'description_generator'} = '';
4662 push @newsections, $section;
4663 my %calls_detail = %$section;
4664 $calls_detail{'post_total'} = '';
4665 $calls_detail{'sort_weight'} = '';
4666 $calls_detail{'description_generator'} = sub { '' };
4667 $calls_detail{'header_generator'} = sub {
4668 return ' & Date/Time & Called Number & Duration & Price'
4669 if $format eq 'latex';
4672 $calls_detail{'description'} = 'Calls Detail: '
4673 . $section->{'phonenum'};
4674 push @newsections, \%calls_detail;
4678 # after this, each usage class is collapsed/summarized into a single
4679 # line under the Calls Summary section
4680 foreach my $newsection ( @newsections ) {
4681 if($newsection->{'post_total'}) { # this means Calls Summary
4682 foreach my $section ( @sections ) {
4683 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4684 && !$section->{'post_total'});
4685 my $newdesc = $section->{'description'};
4686 my $tn = $section->{'phonenum'};
4687 $newdesc =~ s/$tn//g;
4688 my $line = { ext_description => [],
4692 calls => $section->{'calls'},
4693 section => $newsection,
4694 duration => $section->{'duration'},
4695 description => $newdesc,
4696 amount => sprintf("%.2f",$section->{'amount'}),
4697 product_code => 'N/A',
4699 push @newlines, $line;
4704 # after this, Calls Details is populated with all CDRs
4705 foreach my $newsection ( @newsections ) {
4706 if(!$newsection->{'post_total'}) { # this means Calls Details
4707 foreach my $line ( @lines ) {
4708 next unless (scalar(@{$line->{'ext_description'}}) &&
4709 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4711 my @extdesc = @{$line->{'ext_description'}};
4713 foreach my $extdesc ( @extdesc ) {
4714 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4715 push @newextdesc, $extdesc;
4717 $line->{'ext_description'} = \@newextdesc;
4718 $line->{'section'} = $newsection;
4719 push @newlines, $line;
4724 return(\@newsections, \@newlines);
4727 return(\@sections, \@lines);
4731 sub _items { # seems to be unused
4734 #my @display = scalar(@_)
4736 # : qw( _items_previous _items_pkg );
4737 # #: qw( _items_pkg );
4738 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4739 my @display = qw( _items_previous _items_pkg );
4742 foreach my $display ( @display ) {
4743 push @b, $self->$display(@_);
4748 sub _items_previous {
4750 my $conf = $self->conf;
4751 my $cust_main = $self->cust_main;
4752 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4754 foreach ( @pr_cust_bill ) {
4755 my $date = $conf->exists('invoice_show_prior_due_date')
4756 ? 'due '. $_->due_date2str($date_format)
4757 : time2str($date_format, $_->_date);
4759 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4760 #'pkgpart' => 'N/A',
4762 'amount' => sprintf("%.2f", $_->owed),
4768 # 'description' => 'Previous Balance',
4769 # #'pkgpart' => 'N/A',
4770 # 'pkgnum' => 'N/A',
4771 # 'amount' => sprintf("%10.2f", $pr_total ),
4772 # 'ext_description' => [ map {
4773 # "Invoice ". $_->invnum.
4774 # " (". time2str("%x",$_->_date). ") ".
4775 # sprintf("%10.2f", $_->owed)
4776 # } @pr_cust_bill ],
4781 =item _items_pkg [ OPTIONS ]
4783 Return line item hashes for each package item on this invoice. Nearly
4786 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4788 The only OPTIONS accepted is 'section', which may point to a hashref
4789 with a key named 'condensed', which may have a true value. If it
4790 does, this method tries to merge identical items into items with
4791 'quantity' equal to the number of items (not the sum of their
4792 separate quantities, for some reason).
4800 warn "$me _items_pkg searching for all package line items\n"
4803 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4805 warn "$me _items_pkg filtering line items\n"
4807 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4809 if ($options{section} && $options{section}->{condensed}) {
4811 warn "$me _items_pkg condensing section\n"
4815 local $Storable::canonical = 1;
4816 foreach ( @items ) {
4818 delete $item->{ref};
4819 delete $item->{ext_description};
4820 my $key = freeze($item);
4821 $itemshash{$key} ||= 0;
4822 $itemshash{$key} ++; # += $item->{quantity};
4824 @items = sort { $a->{description} cmp $b->{description} }
4825 map { my $i = thaw($_);
4826 $i->{quantity} = $itemshash{$_};
4828 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4834 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4841 return 0 unless $a->itemdesc cmp $b->itemdesc;
4842 return -1 if $b->itemdesc eq 'Tax';
4843 return 1 if $a->itemdesc eq 'Tax';
4844 return -1 if $b->itemdesc eq 'Other surcharges';
4845 return 1 if $a->itemdesc eq 'Other surcharges';
4846 $a->itemdesc cmp $b->itemdesc;
4851 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4852 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4855 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4857 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4858 list of hashrefs describing the line items they generate on the invoice.
4860 OPTIONS may include:
4862 format: the invoice format.
4864 escape_function: the function used to escape strings.
4866 DEPRECATED? (expensive, mostly unused?)
4867 format_function: the function used to format CDRs.
4869 section: a hashref containing 'description'; if this is present,
4870 cust_bill_pkg_display records not belonging to this section are
4873 multisection: a flag indicating that this is a multisection invoice,
4874 which does something complicated.
4876 multilocation: a flag to display the location label for the package.
4878 Returns a list of hashrefs, each of which may contain:
4880 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4881 ext_description, which is an arrayref of detail lines to show below
4886 sub _items_cust_bill_pkg {
4888 my $conf = $self->conf;
4889 my $cust_bill_pkgs = shift;
4892 my $format = $opt{format} || '';
4893 my $escape_function = $opt{escape_function} || sub { shift };
4894 my $format_function = $opt{format_function} || '';
4895 my $no_usage = $opt{no_usage} || '';
4896 my $unsquelched = $opt{unsquelched} || ''; #unused
4897 my $section = $opt{section}->{description} if $opt{section};
4898 my $summary_page = $opt{summary_page} || ''; #unused
4899 my $multilocation = $opt{multilocation} || '';
4900 my $multisection = $opt{multisection} || '';
4901 my $discount_show_always = 0;
4903 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4905 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4908 my ($s, $r, $u) = ( undef, undef, undef );
4909 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4912 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4913 if ( $_ && !$cust_bill_pkg->hidden ) {
4914 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4915 $_->{amount} =~ s/^\-0\.00$/0.00/;
4916 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4918 if $_->{amount} != 0
4919 || $discount_show_always
4920 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4921 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4927 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4929 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4930 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4933 foreach my $display ( grep { defined($section)
4934 ? $_->section eq $section
4937 #grep { !$_->summary || !$summary_page } # bunk!
4938 grep { !$_->summary || $multisection }
4939 @cust_bill_pkg_display
4943 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4944 $display->billpkgdisplaynum. "\n"
4947 my $type = $display->type;
4949 my $desc = $cust_bill_pkg->desc;
4950 $desc = substr($desc, 0, $maxlength). '...'
4951 if $format eq 'latex' && length($desc) > $maxlength;
4953 my %details_opt = ( 'format' => $format,
4954 'escape_function' => $escape_function,
4955 'format_function' => $format_function,
4956 'no_usage' => $opt{'no_usage'},
4959 if ( $cust_bill_pkg->pkgnum > 0 ) {
4961 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4964 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4966 # start/end dates for invoice formats that do nonstandard
4968 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4970 if ( (!$type || $type eq 'S')
4971 && ( $cust_bill_pkg->setup != 0
4972 || $cust_bill_pkg->setup_show_zero
4977 warn "$me _items_cust_bill_pkg adding setup\n"
4980 my $description = $desc;
4981 $description .= ' Setup'
4982 if $cust_bill_pkg->recur != 0
4983 || $discount_show_always
4984 || $cust_bill_pkg->recur_show_zero;
4987 unless ( $cust_pkg->part_pkg->hide_svc_detail
4988 || $cust_bill_pkg->hidden )
4991 push @d, map &{$escape_function}($_),
4992 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4993 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4995 if ( $multilocation ) {
4996 my $loc = $cust_pkg->location_label;
4997 $loc = substr($loc, 0, $maxlength). '...'
4998 if $format eq 'latex' && length($loc) > $maxlength;
4999 push @d, &{$escape_function}($loc);
5002 } #unless hiding service details
5004 push @d, $cust_bill_pkg->details(%details_opt)
5005 if $cust_bill_pkg->recur == 0;
5007 if ( $cust_bill_pkg->hidden ) {
5008 $s->{amount} += $cust_bill_pkg->setup;
5009 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5010 push @{ $s->{ext_description} }, @d;
5014 description => $description,
5015 #pkgpart => $part_pkg->pkgpart,
5016 pkgnum => $cust_bill_pkg->pkgnum,
5017 amount => $cust_bill_pkg->setup,
5018 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5019 unit_amount => $cust_bill_pkg->unitsetup,
5020 quantity => $cust_bill_pkg->quantity,
5021 ext_description => \@d,
5027 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5029 $cust_bill_pkg->recur != 0
5030 || $cust_bill_pkg->setup == 0
5031 || $discount_show_always
5032 || $cust_bill_pkg->recur_show_zero
5037 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5040 my $is_summary = $display->summary;
5041 my $description = ($is_summary && $type && $type eq 'U')
5042 ? "Usage charges" : $desc;
5044 #pry be a bit more efficient to look some of this conf stuff up
5047 $conf->exists('disable_line_item_date_ranges')
5048 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5051 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5052 $cust_main->agentnum
5054 if ( defined($date_style) && $date_style eq 'month_of' ) {
5055 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5056 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5057 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5058 $cust_main->agentnum
5060 $desc .= ' ' unless $desc =~ /\s$/;
5061 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5063 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5064 " - ". time2str($date_format, $cust_bill_pkg->edate);
5066 $description .= " ($time_period)";
5070 my @seconds = (); # for display of usage info
5072 #at least until cust_bill_pkg has "past" ranges in addition to
5073 #the "future" sdate/edate ones... see #3032
5074 my @dates = ( $self->_date );
5075 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5076 push @dates, $prev->sdate if $prev;
5077 push @dates, undef if !$prev;
5079 unless ( $cust_pkg->part_pkg->hide_svc_detail
5080 || $cust_bill_pkg->itemdesc
5081 || $cust_bill_pkg->hidden
5082 || $is_summary && $type && $type eq 'U' )
5085 warn "$me _items_cust_bill_pkg adding service details\n"
5088 push @d, map &{$escape_function}($_),
5089 $cust_pkg->h_labels_short(@dates, 'I')
5090 #$cust_bill_pkg->edate,
5091 #$cust_bill_pkg->sdate)
5092 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5094 warn "$me _items_cust_bill_pkg done adding service details\n"
5097 if ( $multilocation ) {
5098 my $loc = $cust_pkg->location_label;
5099 $loc = substr($loc, 0, $maxlength). '...'
5100 if $format eq 'latex' && length($loc) > $maxlength;
5101 push @d, &{$escape_function}($loc);
5104 # Display of seconds_since_sqlradacct:
5105 # On the invoice, when processing @detail_items, look for a field
5106 # named 'seconds'. This will contain total seconds for each
5107 # service, in the same order as @ext_description. For services
5108 # that don't support this it will show undef.
5109 if ( $conf->exists('svc_acct-usage_seconds')
5110 and ! $cust_bill_pkg->pkgpart_override ) {
5111 foreach my $cust_svc (
5112 $cust_pkg->h_cust_svc(@dates, 'I')
5115 # eval because not having any part_export_usage exports
5116 # is a fatal error, last_bill/_date because that's how
5117 # sqlradius_hour billing does it
5119 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5121 push @seconds, $sec;
5123 } #if svc_acct-usage_seconds
5127 unless ( $is_summary ) {
5128 warn "$me _items_cust_bill_pkg adding details\n"
5131 #instead of omitting details entirely in this case (unwanted side
5132 # effects), just omit CDRs
5133 $details_opt{'no_usage'} = 1
5134 if $type && $type eq 'R';
5136 push @d, $cust_bill_pkg->details(%details_opt);
5139 warn "$me _items_cust_bill_pkg calculating amount\n"
5144 $amount = $cust_bill_pkg->recur;
5145 } elsif ($type eq 'R') {
5146 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5147 } elsif ($type eq 'U') {
5148 $amount = $cust_bill_pkg->usage;
5151 if ( !$type || $type eq 'R' ) {
5153 warn "$me _items_cust_bill_pkg adding recur\n"
5156 if ( $cust_bill_pkg->hidden ) {
5157 $r->{amount} += $amount;
5158 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5159 push @{ $r->{ext_description} }, @d;
5162 description => $description,
5163 #pkgpart => $part_pkg->pkgpart,
5164 pkgnum => $cust_bill_pkg->pkgnum,
5166 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5167 unit_amount => $cust_bill_pkg->unitrecur,
5168 quantity => $cust_bill_pkg->quantity,
5170 ext_description => \@d,
5172 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5175 } else { # $type eq 'U'
5177 warn "$me _items_cust_bill_pkg adding usage\n"
5180 if ( $cust_bill_pkg->hidden ) {
5181 $u->{amount} += $amount;
5182 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5183 push @{ $u->{ext_description} }, @d;
5186 description => $description,
5187 #pkgpart => $part_pkg->pkgpart,
5188 pkgnum => $cust_bill_pkg->pkgnum,
5190 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5191 unit_amount => $cust_bill_pkg->unitrecur,
5192 quantity => $cust_bill_pkg->quantity,
5194 ext_description => \@d,
5199 } # recurring or usage with recurring charge
5201 } else { #pkgnum tax or one-shot line item (??)
5203 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5206 if ( $cust_bill_pkg->setup != 0 ) {
5208 'description' => $desc,
5209 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5212 if ( $cust_bill_pkg->recur != 0 ) {
5214 'description' => "$desc (".
5215 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5216 time2str($date_format, $cust_bill_pkg->edate). ')',
5217 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5225 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5226 && $conf->exists('discount-show-always'));
5230 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5232 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5233 $_->{amount} =~ s/^\-0\.00$/0.00/;
5234 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5236 if $_->{amount} != 0
5237 || $discount_show_always
5238 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5239 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5243 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5250 sub _items_credits {
5251 my( $self, %opt ) = @_;
5252 my $trim_len = $opt{'trim_len'} || 60;
5256 foreach ( $self->cust_credited ) {
5258 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5260 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5261 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5262 $reason = " ($reason) " if $reason;
5265 #'description' => 'Credit ref\#'. $_->crednum.
5266 # " (". time2str("%x",$_->cust_credit->_date) .")".
5268 'description' => $self->mt('Credit applied').' '.
5269 time2str($date_format,$_->cust_credit->_date). $reason,
5270 'amount' => sprintf("%.2f",$_->amount),
5278 sub _items_payments {
5282 #get & print payments
5283 foreach ( $self->cust_bill_pay ) {
5285 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5288 'description' => $self->mt('Payment received').' '.
5289 time2str($date_format,$_->cust_pay->_date ),
5290 'amount' => sprintf("%.2f", $_->amount )
5298 =item _items_discounts_avail
5300 Returns an array of line item hashrefs representing available term discounts
5301 for this invoice. This makes the same assumptions that apply to term
5302 discounts in general: that the package is billed monthly, at a flat rate,
5303 with no usage charges. A prorated first month will be handled, as will
5304 a setup fee if the discount is allowed to apply to setup fees.
5308 sub _items_discounts_avail {
5310 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5312 my %plans = $self->discount_plans;
5314 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5318 my $plan = $plans{$months};
5320 my $term_total = sprintf('%.2f', $plan->discounted_total);
5321 my $percent = sprintf('%.0f',
5322 100 * (1 - $term_total / $plan->base_total) );
5323 my $permonth = sprintf('%.2f', $term_total / $months);
5324 my $detail = $self->mt('discount on item'). ' '.
5325 join(', ', map { "#$_" } $plan->pkgnums)
5328 # discounts for non-integer months don't work anyway
5329 $months = sprintf("%d", $months);
5332 description => $self->mt('Save [_1]% by paying for [_2] months',
5334 amount => $self->mt('[_1] ([_2] per month)',
5335 $term_total, $money_char.$permonth),
5336 ext_description => ($detail || ''),
5339 sort { $b <=> $a } keys %plans;
5343 =item call_details [ OPTION => VALUE ... ]
5345 Returns an array of CSV strings representing the call details for this invoice
5346 The only option available is the boolean prepend_billed_number
5351 my ($self, %opt) = @_;
5353 my $format_function = sub { shift };
5355 if ($opt{prepend_billed_number}) {
5356 $format_function = sub {
5360 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5365 my @details = map { $_->details( 'format_function' => $format_function,
5366 'escape_function' => sub{ return() },
5370 $self->cust_bill_pkg;
5371 my $header = $details[0];
5372 ( $header, grep { $_ ne $header } @details );
5382 =item process_reprint
5386 sub process_reprint {
5387 process_re_X('print', @_);
5390 =item process_reemail
5394 sub process_reemail {
5395 process_re_X('email', @_);
5403 process_re_X('fax', @_);
5411 process_re_X('ftp', @_);
5418 sub process_respool {
5419 process_re_X('spool', @_);
5422 use Storable qw(thaw);
5426 my( $method, $job ) = ( shift, shift );
5427 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5429 my $param = thaw(decode_base64(shift));
5430 warn Dumper($param) if $DEBUG;
5441 my($method, $job, %param ) = @_;
5443 warn "re_X $method for job $job with param:\n".
5444 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5447 #some false laziness w/search/cust_bill.html
5449 my $orderby = 'ORDER BY cust_bill._date';
5451 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5453 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5455 my @cust_bill = qsearch( {
5456 #'select' => "cust_bill.*",
5457 'table' => 'cust_bill',
5458 'addl_from' => $addl_from,
5460 'extra_sql' => $extra_sql,
5461 'order_by' => $orderby,
5465 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5467 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5470 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5471 foreach my $cust_bill ( @cust_bill ) {
5472 $cust_bill->$method();
5474 if ( $job ) { #progressbar foo
5476 if ( time - $min_sec > $last ) {
5477 my $error = $job->update_statustext(
5478 int( 100 * $num / scalar(@cust_bill) )
5480 die $error if $error;
5491 =head1 CLASS METHODS
5497 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5502 my ($class, $start, $end) = @_;
5504 $class->paid_sql($start, $end). ' - '.
5505 $class->credited_sql($start, $end);
5510 Returns an SQL fragment to retreive the net amount (charged minus credited).
5515 my ($class, $start, $end) = @_;
5516 'charged - '. $class->credited_sql($start, $end);
5521 Returns an SQL fragment to retreive the amount paid against this invoice.
5526 my ($class, $start, $end) = @_;
5527 $start &&= "AND cust_bill_pay._date <= $start";
5528 $end &&= "AND cust_bill_pay._date > $end";
5529 $start = '' unless defined($start);
5530 $end = '' unless defined($end);
5531 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5532 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5537 Returns an SQL fragment to retreive the amount credited against this invoice.
5542 my ($class, $start, $end) = @_;
5543 $start &&= "AND cust_credit_bill._date <= $start";
5544 $end &&= "AND cust_credit_bill._date > $end";
5545 $start = '' unless defined($start);
5546 $end = '' unless defined($end);
5547 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5548 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5553 Returns an SQL fragment to retrieve the due date of an invoice.
5554 Currently only supported on PostgreSQL.
5559 my $conf = new FS::Conf;
5563 cust_bill.invoice_terms,
5564 cust_main.invoice_terms,
5565 \''.($conf->config('invoice_default_terms') || '').'\'
5566 ), E\'Net (\\\\d+)\'
5568 ) * 86400 + cust_bill._date'
5571 =item search_sql_where HASHREF
5573 Class method which returns an SQL WHERE fragment to search for parameters
5574 specified in HASHREF. Valid parameters are
5580 List reference of start date, end date, as UNIX timestamps.
5590 List reference of charged limits (exclusive).
5594 List reference of charged limits (exclusive).
5598 flag, return open invoices only
5602 flag, return net invoices only
5606 =item newest_percust
5610 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5614 sub search_sql_where {
5615 my($class, $param) = @_;
5617 warn "$me search_sql_where called with params: \n".
5618 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5624 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5625 push @search, "cust_main.agentnum = $1";
5629 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5630 push @search, "cust_main.refnum = $1";
5634 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5635 push @search, "cust_bill.custnum = $1";
5639 if ( $param->{_date} ) {
5640 my($beginning, $ending) = @{$param->{_date}};
5642 push @search, "cust_bill._date >= $beginning",
5643 "cust_bill._date < $ending";
5647 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5648 push @search, "cust_bill.invnum >= $1";
5650 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5651 push @search, "cust_bill.invnum <= $1";
5655 if ( $param->{charged} ) {
5656 my @charged = ref($param->{charged})
5657 ? @{ $param->{charged} }
5658 : ($param->{charged});
5660 push @search, map { s/^charged/cust_bill.charged/; $_; }
5664 my $owed_sql = FS::cust_bill->owed_sql;
5667 if ( $param->{owed} ) {
5668 my @owed = ref($param->{owed})
5669 ? @{ $param->{owed} }
5671 push @search, map { s/^owed/$owed_sql/; $_; }
5676 push @search, "0 != $owed_sql"
5677 if $param->{'open'};
5678 push @search, '0 != '. FS::cust_bill->net_sql
5682 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5683 if $param->{'days'};
5686 if ( $param->{'newest_percust'} ) {
5688 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5689 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5691 my @newest_where = map { my $x = $_;
5692 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5695 grep ! /^cust_main./, @search;
5696 my $newest_where = scalar(@newest_where)
5697 ? ' AND '. join(' AND ', @newest_where)
5701 push @search, "cust_bill._date = (
5702 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5703 WHERE newest_cust_bill.custnum = cust_bill.custnum
5709 #promised_date - also has an option to accept nulls
5710 if ( $param->{promised_date} ) {
5711 my($beginning, $ending, $null) = @{$param->{promised_date}};
5713 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5714 "cust_bill.promised_date < $ending )" .
5715 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5718 #agent virtualization
5719 my $curuser = $FS::CurrentUser::CurrentUser;
5720 if ( $curuser->username eq 'fs_queue'
5721 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5723 my $newuser = qsearchs('access_user', {
5724 'username' => $username,
5728 $curuser = $newuser;
5730 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5733 push @search, $curuser->agentnums_sql;
5735 join(' AND ', @search );
5747 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5748 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base