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; }
398 =item enable_previous
400 Whether to show the 'Previous Charges' section when printing this invoice.
401 The negation of the 'disable_previous_balance' config setting.
405 sub enable_previous {
407 my $agentnum = $self->cust_main->agentnum;
408 !$self->conf->exists('disable_previous_balance', $agentnum);
413 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
420 { 'table' => 'cust_bill_pkg',
421 'hashref' => { 'invnum' => $self->invnum },
422 'order_by' => 'ORDER BY billpkgnum',
427 =item cust_bill_pkg_pkgnum PKGNUM
429 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
434 sub cust_bill_pkg_pkgnum {
435 my( $self, $pkgnum ) = @_;
437 { 'table' => 'cust_bill_pkg',
438 'hashref' => { 'invnum' => $self->invnum,
441 'order_by' => 'ORDER BY billpkgnum',
448 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
455 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
456 $self->cust_bill_pkg;
458 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
463 Returns true if any of the packages (or their definitions) corresponding to the
464 line items for this invoice have the no_auto flag set.
470 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
473 =item open_cust_bill_pkg
475 Returns the open line items for this invoice.
477 Note that cust_bill_pkg with both setup and recur fees are returned as two
478 separate line items, each with only one fee.
482 # modeled after cust_main::open_cust_bill
483 sub open_cust_bill_pkg {
486 # grep { $_->owed > 0 } $self->cust_bill_pkg
488 my %other = ( 'recur' => 'setup',
489 'setup' => 'recur', );
491 foreach my $field ( qw( recur setup )) {
492 push @open, map { $_->set( $other{$field}, 0 ); $_; }
493 grep { $_->owed($field) > 0 }
494 $self->cust_bill_pkg;
500 =item cust_bill_event
502 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
506 sub cust_bill_event {
508 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
511 =item num_cust_bill_event
513 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
517 sub num_cust_bill_event {
520 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
521 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
522 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
523 $sth->fetchrow_arrayref->[0];
528 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
532 #false laziness w/cust_pkg.pm
536 'table' => 'cust_event',
537 'addl_from' => 'JOIN part_event USING ( eventpart )',
538 'hashref' => { 'tablenum' => $self->invnum },
539 'extra_sql' => " AND eventtable = 'cust_bill' ",
545 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
549 #false laziness w/cust_pkg.pm
553 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
554 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
555 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
556 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
557 $sth->fetchrow_arrayref->[0];
562 Returns the customer (see L<FS::cust_main>) for this invoice.
568 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
571 =item cust_suspend_if_balance_over AMOUNT
573 Suspends the customer associated with this invoice if the total amount owed on
574 this invoice and all older invoices is greater than the specified amount.
576 Returns a list: an empty list on success or a list of errors.
580 sub cust_suspend_if_balance_over {
581 my( $self, $amount ) = ( shift, shift );
582 my $cust_main = $self->cust_main;
583 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
586 $cust_main->suspend(@_);
592 Depreciated. See the cust_credited method.
594 #Returns a list consisting of the total previous credited (see
595 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
596 #outstanding credits (FS::cust_credit objects).
602 croak "FS::cust_bill->cust_credit depreciated; see ".
603 "FS::cust_bill->cust_credit_bill";
606 #my @cust_credit = sort { $a->_date <=> $b->_date }
607 # grep { $_->credited != 0 && $_->_date < $self->_date }
608 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
610 #foreach (@cust_credit) { $total += $_->credited; }
611 #$total, @cust_credit;
616 Depreciated. See the cust_bill_pay method.
618 #Returns all payments (see L<FS::cust_pay>) for this invoice.
624 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
626 #sort { $a->_date <=> $b->_date }
627 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
633 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
636 sub cust_bill_pay_batch {
638 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
643 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
649 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
650 sort { $a->_date <=> $b->_date }
651 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
656 =item cust_credit_bill
658 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
664 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
665 sort { $a->_date <=> $b->_date }
666 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
670 sub cust_credit_bill {
671 shift->cust_credited(@_);
674 #=item cust_bill_pay_pkgnum PKGNUM
676 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
677 #with matching pkgnum.
681 #sub cust_bill_pay_pkgnum {
682 # my( $self, $pkgnum ) = @_;
683 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
684 # sort { $a->_date <=> $b->_date }
685 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
686 # 'pkgnum' => $pkgnum,
691 =item cust_bill_pay_pkg PKGNUM
693 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
694 applied against the matching pkgnum.
698 sub cust_bill_pay_pkg {
699 my( $self, $pkgnum ) = @_;
702 'select' => 'cust_bill_pay_pkg.*',
703 'table' => 'cust_bill_pay_pkg',
704 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
705 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
706 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
707 " AND cust_bill_pkg.pkgnum = $pkgnum",
712 #=item cust_credited_pkgnum PKGNUM
714 #=item cust_credit_bill_pkgnum PKGNUM
716 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
717 #with matching pkgnum.
721 #sub cust_credited_pkgnum {
722 # my( $self, $pkgnum ) = @_;
723 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
724 # sort { $a->_date <=> $b->_date }
725 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
726 # 'pkgnum' => $pkgnum,
731 #sub cust_credit_bill_pkgnum {
732 # shift->cust_credited_pkgnum(@_);
735 =item cust_credit_bill_pkg PKGNUM
737 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
738 applied against the matching pkgnum.
742 sub cust_credit_bill_pkg {
743 my( $self, $pkgnum ) = @_;
746 'select' => 'cust_credit_bill_pkg.*',
747 'table' => 'cust_credit_bill_pkg',
748 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
749 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
750 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
751 " AND cust_bill_pkg.pkgnum = $pkgnum",
756 =item cust_bill_batch
758 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
762 sub cust_bill_batch {
764 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
769 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
770 hash keyed by term length.
776 FS::discount_plan->all($self);
781 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
788 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
790 foreach (@taxlines) { $total += $_->setup; }
796 Returns the amount owed (still outstanding) on this invoice, which is charged
797 minus all payment applications (see L<FS::cust_bill_pay>) and credit
798 applications (see L<FS::cust_credit_bill>).
804 my $balance = $self->charged;
805 $balance -= $_->amount foreach ( $self->cust_bill_pay );
806 $balance -= $_->amount foreach ( $self->cust_credited );
807 $balance = sprintf( "%.2f", $balance);
808 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
813 my( $self, $pkgnum ) = @_;
815 #my $balance = $self->charged;
817 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
819 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
820 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
822 $balance = sprintf( "%.2f", $balance);
823 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
829 Returns true if this invoice should be hidden. See the
830 selfservice-hide_invoices-taxclass configuraiton setting.
836 my $conf = $self->conf;
837 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
839 my @cust_bill_pkg = $self->cust_bill_pkg;
840 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
841 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
844 =item apply_payments_and_credits [ OPTION => VALUE ... ]
846 Applies unapplied payments and credits to this invoice.
848 A hash of optional arguments may be passed. Currently "manual" is supported.
849 If true, a payment receipt is sent instead of a statement when
850 'payment_receipt_email' configuration option is set.
852 If there is an error, returns the error, otherwise returns false.
856 sub apply_payments_and_credits {
857 my( $self, %options ) = @_;
858 my $conf = $self->conf;
860 local $SIG{HUP} = 'IGNORE';
861 local $SIG{INT} = 'IGNORE';
862 local $SIG{QUIT} = 'IGNORE';
863 local $SIG{TERM} = 'IGNORE';
864 local $SIG{TSTP} = 'IGNORE';
865 local $SIG{PIPE} = 'IGNORE';
867 my $oldAutoCommit = $FS::UID::AutoCommit;
868 local $FS::UID::AutoCommit = 0;
871 $self->select_for_update; #mutex
873 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
874 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
876 if ( $conf->exists('pkg-balances') ) {
877 # limit @payments & @credits to those w/ a pkgnum grepped from $self
878 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
879 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
880 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
883 while ( $self->owed > 0 and ( @payments || @credits ) ) {
886 if ( @payments && @credits ) {
888 #decide which goes first by weight of top (unapplied) line item
890 my @open_lineitems = $self->open_cust_bill_pkg;
893 max( map { $_->part_pkg->pay_weight || 0 }
898 my $max_credit_weight =
899 max( map { $_->part_pkg->credit_weight || 0 }
905 #if both are the same... payments first? it has to be something
906 if ( $max_pay_weight >= $max_credit_weight ) {
912 } elsif ( @payments ) {
914 } elsif ( @credits ) {
917 die "guru meditation #12 and 35";
921 if ( $app eq 'pay' ) {
923 my $payment = shift @payments;
924 $unapp_amount = $payment->unapplied;
925 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
926 $app->pkgnum( $payment->pkgnum )
927 if $conf->exists('pkg-balances') && $payment->pkgnum;
929 } elsif ( $app eq 'credit' ) {
931 my $credit = shift @credits;
932 $unapp_amount = $credit->credited;
933 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
934 $app->pkgnum( $credit->pkgnum )
935 if $conf->exists('pkg-balances') && $credit->pkgnum;
938 die "guru meditation #12 and 35";
942 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
943 warn "owed_pkgnum ". $app->pkgnum;
944 $owed = $self->owed_pkgnum($app->pkgnum);
948 next unless $owed > 0;
950 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
951 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
953 $app->invnum( $self->invnum );
955 my $error = $app->insert(%options);
957 $dbh->rollback if $oldAutoCommit;
958 return "Error inserting ". $app->table. " record: $error";
960 die $error if $error;
964 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
969 =item generate_email OPTION => VALUE ...
977 sender address, required
981 alternate template name, optional
985 text attachment arrayref, optional
989 email subject, optional
993 notice name instead of "Invoice", optional
997 Returns an argument list to be passed to L<FS::Misc::send_email>.
1003 sub generate_email {
1007 my $conf = $self->conf;
1009 my $me = '[FS::cust_bill::generate_email]';
1012 'from' => $args{'from'},
1013 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1017 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1018 'template' => $args{'template'},
1019 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1020 'no_coupon' => $args{'no_coupon'},
1023 my $cust_main = $self->cust_main;
1025 if (ref($args{'to'}) eq 'ARRAY') {
1026 $return{'to'} = $args{'to'};
1028 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1029 $cust_main->invoicing_list
1033 if ( $conf->exists('invoice_html') ) {
1035 warn "$me creating HTML/text multipart message"
1038 $return{'nobody'} = 1;
1040 my $alternative = build MIME::Entity
1041 'Type' => 'multipart/alternative',
1042 #'Encoding' => '7bit',
1043 'Disposition' => 'inline'
1047 if ( $conf->exists('invoice_email_pdf')
1048 and scalar($conf->config('invoice_email_pdf_note')) ) {
1050 warn "$me using 'invoice_email_pdf_note' in multipart message"
1052 $data = [ map { $_ . "\n" }
1053 $conf->config('invoice_email_pdf_note')
1058 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1060 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1061 $data = $args{'print_text'};
1063 $data = [ $self->print_text(\%opt) ];
1068 $alternative->attach(
1069 'Type' => 'text/plain',
1070 'Encoding' => 'quoted-printable',
1071 #'Encoding' => '7bit',
1073 'Disposition' => 'inline',
1080 if ( $conf->exists('invoice_email_pdf')
1081 and scalar($conf->config('invoice_email_pdf_note')) ) {
1083 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1087 $args{'from'} =~ /\@([\w\.\-]+)/;
1088 my $from = $1 || 'example.com';
1089 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1092 my $agentnum = $cust_main->agentnum;
1093 if ( defined($args{'template'}) && length($args{'template'})
1094 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1097 $logo = 'logo_'. $args{'template'}. '.png';
1101 my $image_data = $conf->config_binary( $logo, $agentnum);
1103 $image = build MIME::Entity
1104 'Type' => 'image/png',
1105 'Encoding' => 'base64',
1106 'Data' => $image_data,
1107 'Filename' => 'logo.png',
1108 'Content-ID' => "<$content_id>",
1111 if ($conf->exists('invoice-barcode')) {
1112 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1113 $barcode = build MIME::Entity
1114 'Type' => 'image/png',
1115 'Encoding' => 'base64',
1116 'Data' => $self->invoice_barcode(0),
1117 'Filename' => 'barcode.png',
1118 'Content-ID' => "<$barcode_content_id>",
1120 $opt{'barcode_cid'} = $barcode_content_id;
1123 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1126 $alternative->attach(
1127 'Type' => 'text/html',
1128 'Encoding' => 'quoted-printable',
1129 'Data' => [ '<html>',
1132 ' '. encode_entities($return{'subject'}),
1135 ' <body bgcolor="#e8e8e8">',
1140 'Disposition' => 'inline',
1141 #'Filename' => 'invoice.pdf',
1145 my @otherparts = ();
1146 if ( $cust_main->email_csv_cdr ) {
1148 push @otherparts, build MIME::Entity
1149 'Type' => 'text/csv',
1150 'Encoding' => '7bit',
1151 'Data' => [ map { "$_\n" }
1152 $self->call_details('prepend_billed_number' => 1)
1154 'Disposition' => 'attachment',
1155 'Filename' => 'usage-'. $self->invnum. '.csv',
1160 if ( $conf->exists('invoice_email_pdf') ) {
1165 # multipart/alternative
1171 my $related = build MIME::Entity 'Type' => 'multipart/related',
1172 'Encoding' => '7bit';
1174 #false laziness w/Misc::send_email
1175 $related->head->replace('Content-type',
1176 $related->mime_type.
1177 '; boundary="'. $related->head->multipart_boundary. '"'.
1178 '; type=multipart/alternative'
1181 $related->add_part($alternative);
1183 $related->add_part($image) if $image;
1185 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1187 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1191 #no other attachment:
1193 # multipart/alternative
1198 $return{'content-type'} = 'multipart/related';
1199 if ($conf->exists('invoice-barcode') && $barcode) {
1200 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1202 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1204 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1205 #$return{'disposition'} = 'inline';
1211 if ( $conf->exists('invoice_email_pdf') ) {
1212 warn "$me creating PDF attachment"
1215 #mime parts arguments a la MIME::Entity->build().
1216 $return{'mimeparts'} = [
1217 { $self->mimebuild_pdf(\%opt) }
1221 if ( $conf->exists('invoice_email_pdf')
1222 and scalar($conf->config('invoice_email_pdf_note')) ) {
1224 warn "$me using 'invoice_email_pdf_note'"
1226 $return{'body'} = [ map { $_ . "\n" }
1227 $conf->config('invoice_email_pdf_note')
1232 warn "$me not using 'invoice_email_pdf_note'"
1234 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1235 $return{'body'} = $args{'print_text'};
1237 $return{'body'} = [ $self->print_text(\%opt) ];
1250 Returns a list suitable for passing to MIME::Entity->build(), representing
1251 this invoice as PDF attachment.
1258 'Type' => 'application/pdf',
1259 'Encoding' => 'base64',
1260 'Data' => [ $self->print_pdf(@_) ],
1261 'Disposition' => 'attachment',
1262 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1266 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1268 Sends this invoice to the destinations configured for this customer: sends
1269 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1271 Options can be passed as a hashref (recommended) or as a list of up to
1272 four values for templatename, agentnum, invoice_from and amount.
1274 I<template>, if specified, is the name of a suffix for alternate invoices.
1276 I<agentnum>, if specified, means that this invoice will only be sent for customers
1277 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1278 single agent) or an arrayref of agentnums.
1280 I<invoice_from>, if specified, overrides the default email invoice From: address.
1282 I<amount>, if specified, only sends the invoice if the total amount owed on this
1283 invoice and all older invoices is greater than the specified amount.
1285 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1289 sub queueable_send {
1292 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1293 or die "invalid invoice number: " . $opt{invnum};
1295 my @args = ( $opt{template}, $opt{agentnum} );
1296 push @args, $opt{invoice_from}
1297 if exists($opt{invoice_from}) && $opt{invoice_from};
1299 my $error = $self->send( @args );
1300 die $error if $error;
1306 my $conf = $self->conf;
1308 my( $template, $invoice_from, $notice_name );
1310 my $balance_over = 0;
1314 $template = $opt->{'template'} || '';
1315 if ( $agentnums = $opt->{'agentnum'} ) {
1316 $agentnums = [ $agentnums ] unless ref($agentnums);
1318 $invoice_from = $opt->{'invoice_from'};
1319 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1320 $notice_name = $opt->{'notice_name'};
1322 $template = scalar(@_) ? shift : '';
1323 if ( scalar(@_) && $_[0] ) {
1324 $agentnums = ref($_[0]) ? shift : [ shift ];
1326 $invoice_from = shift if scalar(@_);
1327 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1330 my $cust_main = $self->cust_main;
1332 return 'N/A' unless ! $agentnums
1333 or grep { $_ == $cust_main->agentnum } @$agentnums;
1336 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1338 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1339 $conf->config('invoice_from', $cust_main->agentnum );
1342 'template' => $template,
1343 'invoice_from' => $invoice_from,
1344 'notice_name' => ( $notice_name || 'Invoice' ),
1347 my @invoicing_list = $cust_main->invoicing_list;
1349 #$self->email_invoice(\%opt)
1351 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1352 && ! $self->invoice_noemail;
1354 #$self->print_invoice(\%opt)
1356 if grep { $_ eq 'POST' } @invoicing_list; #postal
1358 $self->fax_invoice(\%opt)
1359 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1365 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1367 Emails this invoice.
1369 Options can be passed as a hashref (recommended) or as a list of up to
1370 two values for templatename and invoice_from.
1372 I<template>, if specified, is the name of a suffix for alternate invoices.
1374 I<invoice_from>, if specified, overrides the default email invoice From: address.
1376 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1380 sub queueable_email {
1383 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1384 or die "invalid invoice number: " . $opt{invnum};
1386 my %args = ( 'template' => $opt{template} );
1387 $args{$_} = $opt{$_}
1388 foreach grep { exists($opt{$_}) && $opt{$_} }
1389 qw( invoice_from notice_name no_coupon );
1391 my $error = $self->email( \%args );
1392 die $error if $error;
1396 #sub email_invoice {
1399 return if $self->hide;
1400 my $conf = $self->conf;
1402 my( $template, $invoice_from, $notice_name, $no_coupon );
1405 $template = $opt->{'template'} || '';
1406 $invoice_from = $opt->{'invoice_from'};
1407 $notice_name = $opt->{'notice_name'} || 'Invoice';
1408 $no_coupon = $opt->{'no_coupon'} || 0;
1410 $template = scalar(@_) ? shift : '';
1411 $invoice_from = shift if scalar(@_);
1412 $notice_name = 'Invoice';
1416 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1417 $conf->config('invoice_from', $self->cust_main->agentnum );
1419 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1420 $self->cust_main->invoicing_list;
1422 if ( ! @invoicing_list ) { #no recipients
1423 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1424 die 'No recipients for customer #'. $self->custnum;
1426 #default: better to notify this person than silence
1427 @invoicing_list = ($invoice_from);
1431 my $subject = $self->email_subject($template);
1433 my $error = send_email(
1434 $self->generate_email(
1435 'from' => $invoice_from,
1436 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1437 'subject' => $subject,
1438 'template' => $template,
1439 'notice_name' => $notice_name,
1440 'no_coupon' => $no_coupon,
1443 die "can't email invoice: $error\n" if $error;
1444 #die "$error\n" if $error;
1450 my $conf = $self->conf;
1452 #my $template = scalar(@_) ? shift : '';
1455 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1458 my $cust_main = $self->cust_main;
1459 my $name = $cust_main->name;
1460 my $name_short = $cust_main->name_short;
1461 my $invoice_number = $self->invnum;
1462 my $invoice_date = $self->_date_pretty;
1464 eval qq("$subject");
1467 =item lpr_data HASHREF | [ TEMPLATE ]
1469 Returns the postscript or plaintext for this invoice as an arrayref.
1471 Options can be passed as a hashref (recommended) or as a single optional value
1474 I<template>, if specified, is the name of a suffix for alternate invoices.
1476 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1482 my $conf = $self->conf;
1483 my( $template, $notice_name );
1486 $template = $opt->{'template'} || '';
1487 $notice_name = $opt->{'notice_name'} || 'Invoice';
1489 $template = scalar(@_) ? shift : '';
1490 $notice_name = 'Invoice';
1494 'template' => $template,
1495 'notice_name' => $notice_name,
1498 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1499 [ $self->$method( \%opt ) ];
1502 =item print HASHREF | [ TEMPLATE ]
1504 Prints this invoice.
1506 Options can be passed as a hashref (recommended) or as a single optional
1509 I<template>, if specified, is the name of a suffix for alternate invoices.
1511 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1515 #sub print_invoice {
1518 return if $self->hide;
1519 my $conf = $self->conf;
1521 my( $template, $notice_name );
1524 $template = $opt->{'template'} || '';
1525 $notice_name = $opt->{'notice_name'} || 'Invoice';
1527 $template = scalar(@_) ? shift : '';
1528 $notice_name = 'Invoice';
1532 'template' => $template,
1533 'notice_name' => $notice_name,
1536 if($conf->exists('invoice_print_pdf')) {
1537 # Add the invoice to the current batch.
1538 $self->batch_invoice(\%opt);
1541 do_print $self->lpr_data(\%opt);
1545 =item fax_invoice HASHREF | [ TEMPLATE ]
1549 Options can be passed as a hashref (recommended) or as a single optional
1552 I<template>, if specified, is the name of a suffix for alternate invoices.
1554 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1560 return if $self->hide;
1561 my $conf = $self->conf;
1563 my( $template, $notice_name );
1566 $template = $opt->{'template'} || '';
1567 $notice_name = $opt->{'notice_name'} || 'Invoice';
1569 $template = scalar(@_) ? shift : '';
1570 $notice_name = 'Invoice';
1573 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1574 unless $conf->exists('invoice_latex');
1576 my $dialstring = $self->cust_main->getfield('fax');
1580 'template' => $template,
1581 'notice_name' => $notice_name,
1584 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1585 'dialstring' => $dialstring,
1587 die $error if $error;
1591 =item batch_invoice [ HASHREF ]
1593 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1594 isn't an open batch, one will be created.
1599 my ($self, $opt) = @_;
1600 my $bill_batch = $self->get_open_bill_batch;
1601 my $cust_bill_batch = FS::cust_bill_batch->new({
1602 batchnum => $bill_batch->batchnum,
1603 invnum => $self->invnum,
1605 return $cust_bill_batch->insert($opt);
1608 =item get_open_batch
1610 Returns the currently open batch as an FS::bill_batch object, creating a new
1611 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1616 sub get_open_bill_batch {
1618 my $conf = $self->conf;
1619 my $hashref = { status => 'O' };
1620 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1621 ? $self->cust_main->agentnum
1623 my $batch = qsearchs('bill_batch', $hashref);
1624 return $batch if $batch;
1625 $batch = FS::bill_batch->new($hashref);
1626 my $error = $batch->insert;
1627 die $error if $error;
1631 =item ftp_invoice [ TEMPLATENAME ]
1633 Sends this invoice data via FTP.
1635 TEMPLATENAME is unused?
1641 my $conf = $self->conf;
1642 my $template = scalar(@_) ? shift : '';
1645 'protocol' => 'ftp',
1646 'server' => $conf->config('cust_bill-ftpserver'),
1647 'username' => $conf->config('cust_bill-ftpusername'),
1648 'password' => $conf->config('cust_bill-ftppassword'),
1649 'dir' => $conf->config('cust_bill-ftpdir'),
1650 'format' => $conf->config('cust_bill-ftpformat'),
1654 =item spool_invoice [ TEMPLATENAME ]
1656 Spools this invoice data (see L<FS::spool_csv>)
1658 TEMPLATENAME is unused?
1664 my $conf = $self->conf;
1665 my $template = scalar(@_) ? shift : '';
1668 'format' => $conf->config('cust_bill-spoolformat'),
1669 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1673 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1675 Like B<send>, but only sends the invoice if it is the newest open invoice for
1680 sub send_if_newest {
1685 grep { $_->owed > 0 }
1686 qsearch('cust_bill', {
1687 'custnum' => $self->custnum,
1688 #'_date' => { op=>'>', value=>$self->_date },
1689 'invnum' => { op=>'>', value=>$self->invnum },
1696 =item send_csv OPTION => VALUE, ...
1698 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1702 protocol - currently only "ftp"
1708 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1709 and YYMMDDHHMMSS is a timestamp.
1711 See L</print_csv> for a description of the output format.
1716 my($self, %opt) = @_;
1720 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1721 mkdir $spooldir, 0700 unless -d $spooldir;
1723 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1724 my $file = "$spooldir/$tracctnum.csv";
1726 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1728 open(CSV, ">$file") or die "can't open $file: $!";
1736 if ( $opt{protocol} eq 'ftp' ) {
1737 eval "use Net::FTP;";
1739 $net = Net::FTP->new($opt{server}) or die @$;
1741 die "unknown protocol: $opt{protocol}";
1744 $net->login( $opt{username}, $opt{password} )
1745 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1747 $net->binary or die "can't set binary mode";
1749 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1751 $net->put($file) or die "can't put $file: $!";
1761 Spools CSV invoice data.
1767 =item format - 'default' or 'billco'
1769 =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>).
1771 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1773 =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.
1780 my($self, %opt) = @_;
1782 my $cust_main = $self->cust_main;
1784 if ( $opt{'dest'} ) {
1785 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1786 $cust_main->invoicing_list;
1787 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1788 || ! keys %invoicing_list;
1791 if ( $opt{'balanceover'} ) {
1793 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1796 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1797 mkdir $spooldir, 0700 unless -d $spooldir;
1799 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1803 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1804 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1807 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1809 open(CSV, ">>$file") or die "can't open $file: $!";
1810 flock(CSV, LOCK_EX);
1815 if ( lc($opt{'format'}) eq 'billco' ) {
1817 flock(CSV, LOCK_UN);
1822 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1825 open(CSV,">>$file") or die "can't open $file: $!";
1826 flock(CSV, LOCK_EX);
1832 flock(CSV, LOCK_UN);
1839 =item print_csv OPTION => VALUE, ...
1841 Returns CSV data for this invoice.
1845 format - 'default' or 'billco'
1847 Returns a list consisting of two scalars. The first is a single line of CSV
1848 header information for this invoice. The second is one or more lines of CSV
1849 detail information for this invoice.
1851 If I<format> is not specified or "default", the fields of the CSV file are as
1854 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1858 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1860 B<record_type> is C<cust_bill> for the initial header line only. The
1861 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1862 fields are filled in.
1864 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1865 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1868 =item invnum - invoice number
1870 =item custnum - customer number
1872 =item _date - invoice date
1874 =item charged - total invoice amount
1876 =item first - customer first name
1878 =item last - customer first name
1880 =item company - company name
1882 =item address1 - address line 1
1884 =item address2 - address line 1
1894 =item pkg - line item description
1896 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1898 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1900 =item sdate - start date for recurring fee
1902 =item edate - end date for recurring fee
1906 If I<format> is "billco", the fields of the header CSV file are as follows:
1908 +-------------------------------------------------------------------+
1909 | FORMAT HEADER FILE |
1910 |-------------------------------------------------------------------|
1911 | Field | Description | Name | Type | Width |
1912 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1913 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1914 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1915 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1916 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1917 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1918 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1919 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1920 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1921 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1922 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1923 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1924 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1925 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1926 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1927 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1928 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1929 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1930 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1931 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1932 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1933 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1934 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1935 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1936 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1937 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1938 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1939 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1940 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1941 +-------+-------------------------------+------------+------+-------+
1943 If I<format> is "billco", the fields of the detail CSV file are as follows:
1945 FORMAT FOR DETAIL FILE
1947 Field | Description | Name | Type | Width
1948 1 | N/A-Leave Empty | RC | CHAR | 2
1949 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1950 3 | Account Number | TRACCTNUM | CHAR | 15
1951 4 | Invoice Number | TRINVOICE | CHAR | 15
1952 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1953 6 | Transaction Detail | DETAILS | CHAR | 100
1954 7 | Amount | AMT | NUM* | 9
1955 8 | Line Format Control** | LNCTRL | CHAR | 2
1956 9 | Grouping Code | GROUP | CHAR | 2
1957 10 | User Defined | ACCT CODE | CHAR | 15
1962 my($self, %opt) = @_;
1964 eval "use Text::CSV_XS";
1967 my $cust_main = $self->cust_main;
1969 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1971 if ( lc($opt{'format'}) eq 'billco' ) {
1974 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1976 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1978 my( $previous_balance, @unused ) = $self->previous; #previous balance
1980 my $pmt_cr_applied = 0;
1981 $pmt_cr_applied += $_->{'amount'}
1982 foreach ( $self->_items_payments, $self->_items_credits ) ;
1984 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1987 '', # 1 | N/A-Leave Empty CHAR 2
1988 '', # 2 | N/A-Leave Empty CHAR 15
1989 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1990 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1991 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1992 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1993 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1994 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1995 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1996 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1997 '', # 10 | Ancillary Billing Information CHAR 30
1998 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1999 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2002 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2005 $duedate, # 14 | Bill Due Date CHAR 10
2007 $previous_balance, # 15 | Previous Balance NUM* 9
2008 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2009 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2010 $totaldue, # 18 | Total Amt Due NUM* 9
2011 $totaldue, # 19 | Total Amt Due NUM* 9
2012 '', # 20 | 30 Day Aging NUM* 9
2013 '', # 21 | 60 Day Aging NUM* 9
2014 '', # 22 | 90 Day Aging NUM* 9
2015 'N', # 23 | Y/N CHAR 1
2016 '', # 24 | Remittance automation CHAR 100
2017 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2018 $self->custnum, # 26 | Customer Reference Number CHAR 15
2019 '0', # 27 | Federal Tax*** NUM* 9
2020 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2021 '0', # 29 | Other Taxes & Fees*** NUM* 9
2024 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2026 my ($previous_balance) = $self->previous;
2027 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2029 ($_->{pkgnum} || ''),
2032 } $self->_items_pkg;
2035 $cust_main->agentnum,
2036 $cust_main->agent->agent,
2040 $cust_main->address1,
2041 $cust_main->address2,
2047 time2str("%x", $self->_date),
2061 time2str("%x", $self->_date),
2062 sprintf("%.2f", $self->charged),
2063 ( map { $cust_main->getfield($_) }
2064 qw( first last company address1 address2 city state zip country ) ),
2066 ) or die "can't create csv";
2069 my $header = $csv->string. "\n";
2072 if ( lc($opt{'format'}) eq 'billco' ) {
2075 foreach my $item ( $self->_items_pkg ) {
2078 '', # 1 | N/A-Leave Empty CHAR 2
2079 '', # 2 | N/A-Leave Empty CHAR 15
2080 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2081 $self->invnum, # 4 | Invoice Number CHAR 15
2082 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2083 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2084 $item->{'amount'}, # 7 | Amount NUM* 9
2085 '', # 8 | Line Format Control** CHAR 2
2086 '', # 9 | Grouping Code CHAR 2
2087 '', # 10 | User Defined CHAR 15
2090 $detail .= $csv->string. "\n";
2094 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2100 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2102 my($pkg, $setup, $recur, $sdate, $edate);
2103 if ( $cust_bill_pkg->pkgnum ) {
2105 ($pkg, $setup, $recur, $sdate, $edate) = (
2106 $cust_bill_pkg->part_pkg->pkg,
2107 ( $cust_bill_pkg->setup != 0
2108 ? sprintf("%.2f", $cust_bill_pkg->setup )
2110 ( $cust_bill_pkg->recur != 0
2111 ? sprintf("%.2f", $cust_bill_pkg->recur )
2113 ( $cust_bill_pkg->sdate
2114 ? time2str("%x", $cust_bill_pkg->sdate)
2116 ($cust_bill_pkg->edate
2117 ?time2str("%x", $cust_bill_pkg->edate)
2121 } else { #pkgnum tax
2122 next unless $cust_bill_pkg->setup != 0;
2123 $pkg = $cust_bill_pkg->desc;
2124 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2125 ( $sdate, $edate ) = ( '', '' );
2131 ( map { '' } (1..11) ),
2132 ($pkg, $setup, $recur, $sdate, $edate)
2133 ) or die "can't create csv";
2135 $detail .= $csv->string. "\n";
2141 ( $header, $detail );
2147 Pays this invoice with a compliemntary payment. If there is an error,
2148 returns the error, otherwise returns false.
2154 my $cust_pay = new FS::cust_pay ( {
2155 'invnum' => $self->invnum,
2156 'paid' => $self->owed,
2159 'payinfo' => $self->cust_main->payinfo,
2167 Attempts to pay this invoice with a credit card payment via a
2168 Business::OnlinePayment realtime gateway. See
2169 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2170 for supported processors.
2176 $self->realtime_bop( 'CC', @_ );
2181 Attempts to pay this invoice with an electronic check (ACH) payment via a
2182 Business::OnlinePayment realtime gateway. See
2183 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2184 for supported processors.
2190 $self->realtime_bop( 'ECHECK', @_ );
2195 Attempts to pay this invoice with phone bill (LEC) payment via a
2196 Business::OnlinePayment realtime gateway. See
2197 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2198 for supported processors.
2204 $self->realtime_bop( 'LEC', @_ );
2208 my( $self, $method ) = (shift,shift);
2209 my $conf = $self->conf;
2212 my $cust_main = $self->cust_main;
2213 my $balance = $cust_main->balance;
2214 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2215 $amount = sprintf("%.2f", $amount);
2216 return "not run (balance $balance)" unless $amount > 0;
2218 my $description = 'Internet Services';
2219 if ( $conf->exists('business-onlinepayment-description') ) {
2220 my $dtempl = $conf->config('business-onlinepayment-description');
2222 my $agent_obj = $cust_main->agent
2223 or die "can't retreive agent for $cust_main (agentnum ".
2224 $cust_main->agentnum. ")";
2225 my $agent = $agent_obj->agent;
2226 my $pkgs = join(', ',
2227 map { $_->part_pkg->pkg }
2228 grep { $_->pkgnum } $self->cust_bill_pkg
2230 $description = eval qq("$dtempl");
2233 $cust_main->realtime_bop($method, $amount,
2234 'description' => $description,
2235 'invnum' => $self->invnum,
2236 #this didn't do what we want, it just calls apply_payments_and_credits
2238 'apply_to_invoice' => 1,
2241 #this changes application behavior: auto payments
2242 #triggered against a specific invoice are now applied
2243 #to that invoice instead of oldest open.
2249 =item batch_card OPTION => VALUE...
2251 Adds a payment for this invoice to the pending credit card batch (see
2252 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2253 runs the payment using a realtime gateway.
2258 my ($self, %options) = @_;
2259 my $cust_main = $self->cust_main;
2261 $options{invnum} = $self->invnum;
2263 $cust_main->batch_card(%options);
2266 sub _agent_template {
2268 $self->cust_main->agent_template;
2271 sub _agent_invoice_from {
2273 $self->cust_main->agent_invoice_from;
2276 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2278 Returns an text invoice, as a list of lines.
2280 Options can be passed as a hashref (recommended) or as a list of time, template
2281 and then any key/value pairs for any other options.
2283 I<time>, if specified, is used to control the printing of overdue messages. The
2284 default is now. It isn't the date of the invoice; that's the `_date' field.
2285 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2286 L<Time::Local> and L<Date::Parse> for conversion functions.
2288 I<template>, if specified, is the name of a suffix for alternate invoices.
2290 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2296 my( $today, $template, %opt );
2298 %opt = %{ shift() };
2299 $today = delete($opt{'time'}) || '';
2300 $template = delete($opt{template}) || '';
2302 ( $today, $template, %opt ) = @_;
2305 my %params = ( 'format' => 'template' );
2306 $params{'time'} = $today if $today;
2307 $params{'template'} = $template if $template;
2308 $params{$_} = $opt{$_}
2309 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2311 $self->print_generic( %params );
2314 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2316 Internal method - returns a filename of a filled-in LaTeX template for this
2317 invoice (Note: add ".tex" to get the actual filename), and a filename of
2318 an associated logo (with the .eps extension included).
2320 See print_ps and print_pdf for methods that return PostScript and PDF output.
2322 Options can be passed as a hashref (recommended) or as a list of time, template
2323 and then any key/value pairs for any other options.
2325 I<time>, if specified, is used to control the printing of overdue messages. The
2326 default is now. It isn't the date of the invoice; that's the `_date' field.
2327 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2328 L<Time::Local> and L<Date::Parse> for conversion functions.
2330 I<template>, if specified, is the name of a suffix for alternate invoices.
2332 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2338 my $conf = $self->conf;
2339 my( $today, $template, %opt );
2341 %opt = %{ shift() };
2342 $today = delete($opt{'time'}) || '';
2343 $template = delete($opt{template}) || '';
2345 ( $today, $template, %opt ) = @_;
2348 my %params = ( 'format' => 'latex' );
2349 $params{'time'} = $today if $today;
2350 $params{'template'} = $template if $template;
2351 $params{$_} = $opt{$_}
2352 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2354 $template ||= $self->_agent_template;
2356 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2357 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2361 ) or die "can't open temp file: $!\n";
2363 my $agentnum = $self->cust_main->agentnum;
2365 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2366 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2367 or die "can't write temp file: $!\n";
2369 print $lh $conf->config_binary('logo.eps', $agentnum)
2370 or die "can't write temp file: $!\n";
2373 $params{'logo_file'} = $lh->filename;
2375 if($conf->exists('invoice-barcode')){
2376 my $png_file = $self->invoice_barcode($dir);
2377 my $eps_file = $png_file;
2378 $eps_file =~ s/\.png$/.eps/g;
2379 $png_file =~ /(barcode.*png)/;
2381 $eps_file =~ /(barcode.*eps)/;
2384 my $curr_dir = cwd();
2386 # after painfuly long experimentation, it was determined that sam2p won't
2387 # accept : and other chars in the path, no matter how hard I tried to
2388 # escape them, hence the chdir (and chdir back, just to be safe)
2389 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2390 or die "sam2p failed: $!\n";
2394 $params{'barcode_file'} = $eps_file;
2397 my @filled_in = $self->print_generic( %params );
2399 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2403 ) or die "can't open temp file: $!\n";
2404 binmode($fh, ':utf8'); # language support
2405 print $fh join('', @filled_in );
2408 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2409 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2413 =item invoice_barcode DIR_OR_FALSE
2415 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2416 it is taken as the temp directory where the PNG file will be generated and the
2417 PNG file name is returned. Otherwise, the PNG image itself is returned.
2421 sub invoice_barcode {
2422 my ($self, $dir) = (shift,shift);
2424 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2425 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2426 my $gd = $gdbar->plot(Height => 30);
2429 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2433 ) or die "can't open temp file: $!\n";
2434 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2435 my $png_file = $bh->filename;
2442 =item print_generic OPTION => VALUE ...
2444 Internal method - returns a filled-in template for this invoice as a scalar.
2446 See print_ps and print_pdf for methods that return PostScript and PDF output.
2448 Non optional options include
2449 format - latex, html, template
2451 Optional options include
2453 template - a value used as a suffix for a configuration template
2455 time - a value used to control the printing of overdue messages. The
2456 default is now. It isn't the date of the invoice; that's the `_date' field.
2457 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2458 L<Time::Local> and L<Date::Parse> for conversion functions.
2462 unsquelch_cdr - overrides any per customer cdr squelching when true
2464 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2466 locale - override customer's locale
2470 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2471 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2472 # yes: fixed width/plain text printing will be borked
2474 my( $self, %params ) = @_;
2475 my $conf = $self->conf;
2476 my $today = $params{today} ? $params{today} : time;
2477 warn "$me print_generic called on $self with suffix $params{template}\n"
2480 my $format = $params{format};
2481 die "Unknown format: $format"
2482 unless $format =~ /^(latex|html|template)$/;
2484 my $cust_main = $self->cust_main;
2485 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2486 unless $cust_main->payname
2487 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2489 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2490 'html' => [ '<%=', '%>' ],
2491 'template' => [ '{', '}' ],
2494 warn "$me print_generic creating template\n"
2497 #create the template
2498 my $template = $params{template} ? $params{template} : $self->_agent_template;
2499 my $templatefile = "invoice_$format";
2500 $templatefile .= "_$template"
2501 if length($template) && $conf->exists($templatefile."_$template");
2502 my @invoice_template = map "$_\n", $conf->config($templatefile)
2503 or die "cannot load config data $templatefile";
2506 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2507 #change this to a die when the old code is removed
2508 warn "old-style invoice template $templatefile; ".
2509 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2510 $old_latex = 'true';
2511 @invoice_template = _translate_old_latex_format(@invoice_template);
2514 warn "$me print_generic creating T:T object\n"
2517 my $text_template = new Text::Template(
2519 SOURCE => \@invoice_template,
2520 DELIMITERS => $delimiters{$format},
2523 warn "$me print_generic compiling T:T object\n"
2526 $text_template->compile()
2527 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2530 # additional substitution could possibly cause breakage in existing templates
2531 my %convert_maps = (
2533 'notes' => sub { map "$_", @_ },
2534 'footer' => sub { map "$_", @_ },
2535 'smallfooter' => sub { map "$_", @_ },
2536 'returnaddress' => sub { map "$_", @_ },
2537 'coupon' => sub { map "$_", @_ },
2538 'summary' => sub { map "$_", @_ },
2544 s/%%(.*)$/<!-- $1 -->/g;
2545 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2546 s/\\begin\{enumerate\}/<ol>/g;
2548 s/\\end\{enumerate\}/<\/ol>/g;
2549 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2558 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2560 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2565 s/\\\\\*?\s*$/<BR>/;
2566 s/\\hyphenation\{[\w\s\-]+}//;
2571 'coupon' => sub { "" },
2572 'summary' => sub { "" },
2579 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2580 s/\\begin\{enumerate\}//g;
2582 s/\\end\{enumerate\}//g;
2583 s/\\textbf\{(.*)\}/$1/g;
2590 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2592 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2597 s/\\\\\*?\s*$/\n/; # dubious
2598 s/\\hyphenation\{[\w\s\-]+}//;
2602 'coupon' => sub { "" },
2603 'summary' => sub { "" },
2608 # hashes for differing output formats
2609 my %nbsps = ( 'latex' => '~',
2610 'html' => '', # '&nbps;' would be nice
2611 'template' => '', # not used
2613 my $nbsp = $nbsps{$format};
2615 my %escape_functions = ( 'latex' => \&_latex_escape,
2616 'html' => \&_html_escape_nbsp,#\&encode_entities,
2617 'template' => sub { shift },
2619 my $escape_function = $escape_functions{$format};
2620 my $escape_function_nonbsp = ($format eq 'html')
2621 ? \&_html_escape : $escape_function;
2623 my %date_formats = ( 'latex' => $date_format_long,
2624 'html' => $date_format_long,
2627 $date_formats{'html'} =~ s/ / /g;
2629 my $date_format = $date_formats{$format};
2631 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2633 'html' => sub { return '<b>'. shift(). '</b>'
2635 'template' => sub { shift },
2637 my $embolden_function = $embolden_functions{$format};
2639 my %newline_tokens = ( 'latex' => '\\\\',
2643 my $newline_token = $newline_tokens{$format};
2645 warn "$me generating template variables\n"
2648 # generate template variables
2651 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2655 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2661 $returnaddress = join("\n",
2662 $conf->config_orbase("invoice_${format}returnaddress", $template)
2665 } elsif ( grep /\S/,
2666 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2668 my $convert_map = $convert_maps{$format}{'returnaddress'};
2671 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2676 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2678 my $convert_map = $convert_maps{$format}{'returnaddress'};
2679 $returnaddress = join( "\n", &$convert_map(
2680 map { s/( {2,})/'~' x length($1)/eg;
2684 ( $conf->config('company_name', $self->cust_main->agentnum),
2685 $conf->config('company_address', $self->cust_main->agentnum),
2692 my $warning = "Couldn't find a return address; ".
2693 "do you need to set the company_address configuration value?";
2695 $returnaddress = $nbsp;
2696 #$returnaddress = $warning;
2700 warn "$me generating invoice data\n"
2703 my $agentnum = $self->cust_main->agentnum;
2705 my %invoice_data = (
2708 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2709 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2710 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2711 'returnaddress' => $returnaddress,
2712 'agent' => &$escape_function($cust_main->agent->agent),
2715 'invnum' => $self->invnum,
2716 'date' => time2str($date_format, $self->_date),
2717 'today' => time2str($date_format_long, $today),
2718 'terms' => $self->terms,
2719 'template' => $template, #params{'template'},
2720 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2721 'current_charges' => sprintf("%.2f", $self->charged),
2722 'duedate' => $self->due_date2str($rdate_format), #date_format?
2725 'custnum' => $cust_main->display_custnum,
2726 'agent_custid' => &$escape_function($cust_main->agent_custid),
2727 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2728 payname company address1 address2 city state zip fax
2732 'ship_enable' => $conf->exists('invoice-ship_address'),
2733 'unitprices' => $conf->exists('invoice-unitprice'),
2734 'smallernotes' => $conf->exists('invoice-smallernotes'),
2735 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2736 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2738 #layout info -- would be fancy to calc some of this and bury the template
2740 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2741 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2742 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2743 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2744 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2745 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2746 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2747 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2748 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2749 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2751 # better hang on to conf_dir for a while (for old templates)
2752 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2754 #these are only used when doing paged plaintext
2761 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2762 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2763 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2764 # eval to avoid death for unimplemented languages
2765 my $dh = eval { Date::Language->new($info{'name'}) } ||
2766 Date::Language->new(); # fall back to English
2767 # prototype here to silence warnings
2768 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2769 # eventually use this date handle everywhere in here, too
2771 my $min_sdate = 999999999999;
2773 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2774 next unless $cust_bill_pkg->pkgnum > 0;
2775 $min_sdate = $cust_bill_pkg->sdate
2776 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2777 $max_edate = $cust_bill_pkg->edate
2778 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2781 $invoice_data{'bill_period'} = '';
2782 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2783 . " to " . time2str('%e %h', $max_edate)
2784 if ($max_edate != 0 && $min_sdate != 999999999999);
2786 $invoice_data{finance_section} = '';
2787 if ( $conf->config('finance_pkgclass') ) {
2789 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2790 $invoice_data{finance_section} = $pkg_class->categoryname;
2792 $invoice_data{finance_amount} = '0.00';
2793 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2795 my $countrydefault = $conf->config('countrydefault') || 'US';
2796 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2797 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2798 my $method = $prefix.$_;
2799 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2801 $invoice_data{'ship_country'} = ''
2802 if ( $invoice_data{'ship_country'} eq $countrydefault );
2804 $invoice_data{'cid'} = $params{'cid'}
2807 if ( $cust_main->country eq $countrydefault ) {
2808 $invoice_data{'country'} = '';
2810 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2814 $invoice_data{'address'} = \@address;
2816 $cust_main->payname.
2817 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2818 ? " (P.O. #". $cust_main->payinfo. ")"
2822 push @address, $cust_main->company
2823 if $cust_main->company;
2824 push @address, $cust_main->address1;
2825 push @address, $cust_main->address2
2826 if $cust_main->address2;
2828 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2829 push @address, $invoice_data{'country'}
2830 if $invoice_data{'country'};
2832 while (scalar(@address) < 5);
2834 $invoice_data{'logo_file'} = $params{'logo_file'}
2835 if $params{'logo_file'};
2836 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2837 if $params{'barcode_file'};
2838 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2839 if $params{'barcode_img'};
2840 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2841 if $params{'barcode_cid'};
2843 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2844 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2845 #my $balance_due = $self->owed + $pr_total - $cr_total;
2846 my $balance_due = $self->owed + $pr_total;
2848 # the customer's current balance as shown on the invoice before this one
2849 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2851 # the change in balance from that invoice to this one
2852 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2854 # the sum of amount owed on all previous invoices
2855 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2857 # the sum of amount owed on all invoices
2858 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2860 # info from customer's last invoice before this one, for some
2862 $invoice_data{'last_bill'} = {};
2863 my $last_bill = $pr_cust_bill[-1];
2865 $invoice_data{'last_bill'} = {
2866 '_date' => $last_bill->_date, #unformatted
2867 # all we need for now
2871 my $summarypage = '';
2872 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2875 $invoice_data{'summarypage'} = $summarypage;
2877 warn "$me substituting variables in notes, footer, smallfooter\n"
2880 my @include = (qw( notes footer smallfooter ));
2881 push @include, 'coupon' unless $params{'no_coupon'};
2882 foreach my $include (@include) {
2884 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2887 if ( $conf->exists($inc_file, $agentnum)
2888 && length( $conf->config($inc_file, $agentnum) ) ) {
2890 @inc_src = $conf->config($inc_file, $agentnum);
2894 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2896 my $convert_map = $convert_maps{$format}{$include};
2898 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2899 s/--\@\]/$delimiters{$format}[1]/g;
2902 &$convert_map( $conf->config($inc_file, $agentnum) );
2906 my $inc_tt = new Text::Template (
2908 SOURCE => [ map "$_\n", @inc_src ],
2909 DELIMITERS => $delimiters{$format},
2910 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2912 unless ( $inc_tt->compile() ) {
2913 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2914 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2918 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2920 $invoice_data{$include} =~ s/\n+$//
2921 if ($format eq 'latex');
2924 # let invoices use either of these as needed
2925 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2926 ? $cust_main->payinfo : '';
2927 $invoice_data{'po_line'} =
2928 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2929 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2932 my %money_chars = ( 'latex' => '',
2933 'html' => $conf->config('money_char') || '$',
2936 my $money_char = $money_chars{$format};
2938 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2939 'html' => $conf->config('money_char') || '$',
2942 my $other_money_char = $other_money_chars{$format};
2943 $invoice_data{'dollar'} = $other_money_char;
2945 my @detail_items = ();
2946 my @total_items = ();
2950 $invoice_data{'detail_items'} = \@detail_items;
2951 $invoice_data{'total_items'} = \@total_items;
2952 $invoice_data{'buf'} = \@buf;
2953 $invoice_data{'sections'} = \@sections;
2955 warn "$me generating sections\n"
2958 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2959 'subtotal' => $other_money_char.
2960 sprintf('%.2f', $pr_total),
2961 'summarized' => '', #why? $summarypage ? 'Y' : '',
2963 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2964 join(' / ', map { $cust_main->balance_date_range(@$_) }
2965 $self->_prior_month30s
2967 if $conf->exists('invoice_include_aging');
2970 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2971 'subtotal' => $taxtotal, # adjusted below
2973 my $tax_weight = _pkg_category($tax_section->{description})
2974 ? _pkg_category($tax_section->{description})->weight
2976 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2977 $tax_section->{'sort_weight'} = $tax_weight;
2980 my $adjusttotal = 0;
2981 my $adjust_section = { 'description' =>
2982 $self->mt('Credits, Payments, and Adjustments'),
2983 'subtotal' => 0, # adjusted below
2985 my $adjust_weight = _pkg_category($adjust_section->{description})
2986 ? _pkg_category($adjust_section->{description})->weight
2988 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2989 $adjust_section->{'sort_weight'} = $adjust_weight;
2991 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2992 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2993 $invoice_data{'multisection'} = $multisection;
2994 my $late_sections = [];
2995 my $extra_sections = [];
2996 my $extra_lines = ();
2998 my $default_section = { 'description' => '',
3003 if ( $multisection ) {
3004 ($extra_sections, $extra_lines) =
3005 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3006 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3008 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3010 push @detail_items, @$extra_lines if $extra_lines;
3012 $self->_items_sections( $late_sections, # this could stand a refactor
3014 $escape_function_nonbsp,
3018 if ($conf->exists('svc_phone_sections')) {
3019 my ($phone_sections, $phone_lines) =
3020 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3021 push @{$late_sections}, @$phone_sections;
3022 push @detail_items, @$phone_lines;
3024 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3025 my ($accountcode_section, $accountcode_lines) =
3026 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3027 if ( scalar(@$accountcode_lines) ) {
3028 push @{$late_sections}, $accountcode_section;
3029 push @detail_items, @$accountcode_lines;
3032 } else {# not multisection
3033 # make a default section
3034 push @sections, $default_section;
3035 # and calculate the finance charge total, since it won't get done otherwise.
3036 # XXX possibly other totals?
3037 # XXX possibly finance_pkgclass should not be used in this manner?
3038 if ( $conf->exists('finance_pkgclass') ) {
3039 my @finance_charges;
3040 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3041 if ( grep { $_->section eq $invoice_data{finance_section} }
3042 $cust_bill_pkg->cust_bill_pkg_display ) {
3043 # I think these are always setup fees, but just to be sure...
3044 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3047 $invoice_data{finance_amount} =
3048 sprintf('%.2f', sum( @finance_charges ) || 0);
3052 # previous invoice balances in the Previous Charges section if there
3053 # is one, otherwise in the main detail section
3054 if ( $self->can('_items_previous') &&
3055 $self->enable_previous &&
3056 ! $conf->exists('previous_balance-summary_only') ) {
3058 warn "$me adding previous balances\n"
3061 foreach my $line_item ( $self->_items_previous ) {
3064 ext_description => [],
3066 $detail->{'ref'} = $line_item->{'pkgnum'};
3067 $detail->{'quantity'} = 1;
3068 $detail->{'section'} = $multisection ? $previous_section
3070 $detail->{'description'} = &$escape_function($line_item->{'description'});
3071 if ( exists $line_item->{'ext_description'} ) {
3072 @{$detail->{'ext_description'}} = map {
3073 &$escape_function($_);
3074 } @{$line_item->{'ext_description'}};
3076 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3077 $line_item->{'amount'};
3078 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3080 push @detail_items, $detail;
3081 push @buf, [ $detail->{'description'},
3082 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3088 if ( @pr_cust_bill && $self->enable_previous ) {
3089 push @buf, ['','-----------'];
3090 push @buf, [ $self->mt('Total Previous Balance'),
3091 $money_char. sprintf("%10.2f", $pr_total) ];
3095 if ( $conf->exists('svc_phone-did-summary') ) {
3096 warn "$me adding DID summary\n"
3099 my ($didsummary,$minutes) = $self->_did_summary;
3100 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3102 { 'description' => $didsummary_desc,
3103 'ext_description' => [ $didsummary, $minutes ],
3107 foreach my $section (@sections, @$late_sections) {
3109 warn "$me adding section \n". Dumper($section)
3112 # begin some normalization
3113 $section->{'subtotal'} = $section->{'amount'}
3115 && !exists($section->{subtotal})
3116 && exists($section->{amount});
3118 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3119 if ( $invoice_data{finance_section} &&
3120 $section->{'description'} eq $invoice_data{finance_section} );
3122 $section->{'subtotal'} = $other_money_char.
3123 sprintf('%.2f', $section->{'subtotal'})
3126 # continue some normalization
3127 $section->{'amount'} = $section->{'subtotal'}
3131 if ( $section->{'description'} ) {
3132 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3137 warn "$me setting options\n"
3140 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3142 $options{'section'} = $section if $multisection;
3143 $options{'format'} = $format;
3144 $options{'escape_function'} = $escape_function;
3145 $options{'no_usage'} = 1 unless $unsquelched;
3146 $options{'unsquelched'} = $unsquelched;
3147 $options{'summary_page'} = $summarypage;
3148 $options{'skip_usage'} =
3149 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3150 $options{'multilocation'} = $multilocation;
3151 $options{'multisection'} = $multisection;
3153 warn "$me searching for line items\n"
3156 foreach my $line_item ( $self->_items_pkg(%options) ) {
3158 warn "$me adding line item $line_item\n"
3162 ext_description => [],
3164 $detail->{'ref'} = $line_item->{'pkgnum'};
3165 $detail->{'quantity'} = $line_item->{'quantity'};
3166 $detail->{'section'} = $section;
3167 $detail->{'description'} = &$escape_function($line_item->{'description'});
3168 if ( exists $line_item->{'ext_description'} ) {
3169 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3171 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3172 $line_item->{'amount'};
3173 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3174 $line_item->{'unit_amount'};
3175 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3177 $detail->{'sdate'} = $line_item->{'sdate'};
3178 $detail->{'edate'} = $line_item->{'edate'};
3179 $detail->{'seconds'} = $line_item->{'seconds'};
3181 push @detail_items, $detail;
3182 push @buf, ( [ $detail->{'description'},
3183 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3185 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3189 if ( $section->{'description'} ) {
3190 push @buf, ( ['','-----------'],
3191 [ $section->{'description'}. ' sub-total',
3192 $section->{'subtotal'} # already formatted this
3201 $invoice_data{current_less_finance} =
3202 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3204 # create a major section for previous balance if we have major sections,
3205 # or if previous_section is in summary form
3206 if ( ( $multisection && $self->enable_previous )
3207 || $conf->exists('previous_balance-summary_only') )
3209 unshift @sections, $previous_section if $pr_total;
3212 warn "$me adding taxes\n"
3215 foreach my $tax ( $self->_items_tax ) {
3217 $taxtotal += $tax->{'amount'};
3219 my $description = &$escape_function( $tax->{'description'} );
3220 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3222 if ( $multisection ) {
3224 my $money = $old_latex ? '' : $money_char;
3225 push @detail_items, {
3226 ext_description => [],
3229 description => $description,
3230 amount => $money. $amount,
3232 section => $tax_section,
3237 push @total_items, {
3238 'total_item' => $description,
3239 'total_amount' => $other_money_char. $amount,
3244 push @buf,[ $description,
3245 $money_char. $amount,
3252 $total->{'total_item'} = $self->mt('Sub-total');
3253 $total->{'total_amount'} =
3254 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3256 if ( $multisection ) {
3257 $tax_section->{'subtotal'} = $other_money_char.
3258 sprintf('%.2f', $taxtotal);
3259 $tax_section->{'pretotal'} = 'New charges sub-total '.
3260 $total->{'total_amount'};
3261 push @sections, $tax_section if $taxtotal;
3263 unshift @total_items, $total;
3266 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3268 push @buf,['','-----------'];
3269 push @buf,[$self->mt(
3270 (!$self->enable_previous)
3272 : 'Total New Charges'
3274 $money_char. sprintf("%10.2f",$self->charged) ];
3277 # calculate total, possibly including total owed on previous
3282 $item = $conf->config('previous_balance-exclude_from_total')
3283 || 'Total New Charges'
3284 if $conf->exists('previous_balance-exclude_from_total');
3285 my $amount = $self->charged;
3286 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3287 $amount += $pr_total;
3290 $total->{'total_item'} = &$embolden_function($self->mt($item));
3291 $total->{'total_amount'} =
3292 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3293 if ( $multisection ) {
3294 if ( $adjust_section->{'sort_weight'} ) {
3295 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3296 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3298 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3299 $other_money_char. sprintf('%.2f', $self->charged );
3302 push @total_items, $total;
3304 push @buf,['','-----------'];
3307 sprintf( '%10.2f', $amount )
3312 # if we're showing previous invoices, also show previous
3313 # credits and payments
3314 if ( $self->enable_previous
3315 and $self->can('_items_credits')
3316 and $self->can('_items_payments') )
3318 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3321 my $credittotal = 0;
3322 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3325 $total->{'total_item'} = &$escape_function($credit->{'description'});
3326 $credittotal += $credit->{'amount'};
3327 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3328 $adjusttotal += $credit->{'amount'};
3329 if ( $multisection ) {
3330 my $money = $old_latex ? '' : $money_char;
3331 push @detail_items, {
3332 ext_description => [],
3335 description => &$escape_function($credit->{'description'}),
3336 amount => $money. $credit->{'amount'},
3338 section => $adjust_section,
3341 push @total_items, $total;
3345 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3348 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3349 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3353 my $paymenttotal = 0;
3354 foreach my $payment ( $self->_items_payments ) {
3356 $total->{'total_item'} = &$escape_function($payment->{'description'});
3357 $paymenttotal += $payment->{'amount'};
3358 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3359 $adjusttotal += $payment->{'amount'};
3360 if ( $multisection ) {
3361 my $money = $old_latex ? '' : $money_char;
3362 push @detail_items, {
3363 ext_description => [],
3366 description => &$escape_function($payment->{'description'}),
3367 amount => $money. $payment->{'amount'},
3369 section => $adjust_section,
3372 push @total_items, $total;
3374 push @buf, [ $payment->{'description'},
3375 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3378 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3380 if ( $multisection ) {
3381 $adjust_section->{'subtotal'} = $other_money_char.
3382 sprintf('%.2f', $adjusttotal);
3383 push @sections, $adjust_section
3384 unless $adjust_section->{sort_weight};
3387 # create Balance Due message
3390 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3391 $total->{'total_amount'} =
3392 &$embolden_function(
3393 $other_money_char. sprintf('%.2f', $summarypage
3395 $self->billing_balance
3396 : $self->owed + $pr_total
3399 if ( $multisection && !$adjust_section->{sort_weight} ) {
3400 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3401 $total->{'total_amount'};
3403 push @total_items, $total;
3405 push @buf,['','-----------'];
3406 push @buf,[$self->balance_due_msg, $money_char.
3407 sprintf("%10.2f", $balance_due ) ];
3410 if ( $conf->exists('previous_balance-show_credit')
3411 and $cust_main->balance < 0 ) {
3412 my $credit_total = {
3413 'total_item' => &$embolden_function($self->credit_balance_msg),
3414 'total_amount' => &$embolden_function(
3415 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3418 if ( $multisection ) {
3419 $adjust_section->{'posttotal'} .= $newline_token .
3420 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3423 push @total_items, $credit_total;
3425 push @buf,['','-----------'];
3426 push @buf,[$self->credit_balance_msg, $money_char.
3427 sprintf("%10.2f", -$cust_main->balance ) ];
3431 if ( $multisection ) {
3432 if ($conf->exists('svc_phone_sections')) {
3434 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3435 $total->{'total_amount'} =
3436 &$embolden_function(
3437 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3439 my $last_section = pop @sections;
3440 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3441 $total->{'total_amount'};
3442 push @sections, $last_section;
3444 push @sections, @$late_sections
3448 # make a discounts-available section, even without multisection
3449 if ( $conf->exists('discount-show_available')
3450 and my @discounts_avail = $self->_items_discounts_avail ) {
3451 my $discount_section = {
3452 'description' => $self->mt('Discounts Available'),
3457 push @sections, $discount_section;
3458 push @detail_items, map { +{
3459 'ref' => '', #should this be something else?
3460 'section' => $discount_section,
3461 'description' => &$escape_function( $_->{description} ),
3462 'amount' => $money_char . &$escape_function( $_->{amount} ),
3463 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3464 } } @discounts_avail;
3467 # All sections and items are built; now fill in templates.
3468 my @includelist = ();
3469 push @includelist, 'summary' if $summarypage;
3470 foreach my $include ( @includelist ) {
3472 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3475 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3477 @inc_src = $conf->config($inc_file, $agentnum);
3481 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3483 my $convert_map = $convert_maps{$format}{$include};
3485 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3486 s/--\@\]/$delimiters{$format}[1]/g;
3489 &$convert_map( $conf->config($inc_file, $agentnum) );
3493 my $inc_tt = new Text::Template (
3495 SOURCE => [ map "$_\n", @inc_src ],
3496 DELIMITERS => $delimiters{$format},
3497 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3499 unless ( $inc_tt->compile() ) {
3500 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3501 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3505 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3507 $invoice_data{$include} =~ s/\n+$//
3508 if ($format eq 'latex');
3513 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3514 /invoice_lines\((\d*)\)/;
3515 $invoice_lines += $1 || scalar(@buf);
3518 die "no invoice_lines() functions in template?"
3519 if ( $format eq 'template' && !$wasfunc );
3521 if ($format eq 'template') {
3523 if ( $invoice_lines ) {
3524 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3525 $invoice_data{'total_pages'}++
3526 if scalar(@buf) % $invoice_lines;
3529 #setup subroutine for the template
3530 $invoice_data{invoice_lines} = sub {
3531 my $lines = shift || scalar(@buf);
3543 push @collect, split("\n",
3544 $text_template->fill_in( HASH => \%invoice_data )
3546 $invoice_data{'page'}++;
3548 map "$_\n", @collect;
3550 # this is where we actually create the invoice
3551 warn "filling in template for invoice ". $self->invnum. "\n"
3553 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3556 $text_template->fill_in(HASH => \%invoice_data);
3560 # helper routine for generating date ranges
3561 sub _prior_month30s {
3564 [ 1, 2592000 ], # 0-30 days ago
3565 [ 2592000, 5184000 ], # 30-60 days ago
3566 [ 5184000, 7776000 ], # 60-90 days ago
3567 [ 7776000, 0 ], # 90+ days ago
3570 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3571 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3576 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3578 Returns an postscript invoice, as a scalar.
3580 Options can be passed as a hashref (recommended) or as a list of time, template
3581 and then any key/value pairs for any other options.
3583 I<time> an optional value used to control the printing of overdue messages. The
3584 default is now. It isn't the date of the invoice; that's the `_date' field.
3585 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3586 L<Time::Local> and L<Date::Parse> for conversion functions.
3588 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3595 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3596 my $ps = generate_ps($file);
3598 unlink($barcodefile) if $barcodefile;
3603 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3605 Returns an PDF invoice, as a scalar.
3607 Options can be passed as a hashref (recommended) or as a list of time, template
3608 and then any key/value pairs for any other options.
3610 I<time> an optional value used to control the printing of overdue messages. The
3611 default is now. It isn't the date of the invoice; that's the `_date' field.
3612 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3613 L<Time::Local> and L<Date::Parse> for conversion functions.
3615 I<template>, if specified, is the name of a suffix for alternate invoices.
3617 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3624 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3625 my $pdf = generate_pdf($file);
3627 unlink($barcodefile) if $barcodefile;
3632 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3634 Returns an HTML invoice, as a scalar.
3636 I<time> an optional value used to control the printing of overdue messages. The
3637 default is now. It isn't the date of the invoice; that's the `_date' field.
3638 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3639 L<Time::Local> and L<Date::Parse> for conversion functions.
3641 I<template>, if specified, is the name of a suffix for alternate invoices.
3643 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3645 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3646 when emailing the invoice as part of a multipart/related MIME email.
3654 %params = %{ shift() };
3656 $params{'time'} = shift;
3657 $params{'template'} = shift;
3658 $params{'cid'} = shift;
3661 $params{'format'} = 'html';
3663 $self->print_generic( %params );
3666 # quick subroutine for print_latex
3668 # There are ten characters that LaTeX treats as special characters, which
3669 # means that they do not simply typeset themselves:
3670 # # $ % & ~ _ ^ \ { }
3672 # TeX ignores blanks following an escaped character; if you want a blank (as
3673 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3677 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3678 $value =~ s/([<>])/\$$1\$/g;
3684 encode_entities($value);
3688 sub _html_escape_nbsp {
3689 my $value = _html_escape(shift);
3690 $value =~ s/ +/ /g;
3694 #utility methods for print_*
3696 sub _translate_old_latex_format {
3697 warn "_translate_old_latex_format called\n"
3704 if ( $line =~ /^%%Detail\s*$/ ) {
3706 push @template, q![@--!,
3707 q! foreach my $_tr_line (@detail_items) {!,
3708 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3709 q! $_tr_line->{'description'} .= !,
3710 q! "\\tabularnewline\n~~".!,
3711 q! join( "\\tabularnewline\n~~",!,
3712 q! @{$_tr_line->{'ext_description'}}!,
3716 while ( ( my $line_item_line = shift )
3717 !~ /^%%EndDetail\s*$/ ) {
3718 $line_item_line =~ s/'/\\'/g; # nice LTS
3719 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3720 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3721 push @template, " \$OUT .= '$line_item_line';";
3724 push @template, '}',
3727 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3729 push @template, '[@--',
3730 ' foreach my $_tr_line (@total_items) {';
3732 while ( ( my $total_item_line = shift )
3733 !~ /^%%EndTotalDetails\s*$/ ) {
3734 $total_item_line =~ s/'/\\'/g; # nice LTS
3735 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3736 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3737 push @template, " \$OUT .= '$total_item_line';";
3740 push @template, '}',
3744 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3745 push @template, $line;
3751 warn "$_\n" foreach @template;
3759 my $conf = $self->conf;
3761 #check for an invoice-specific override
3762 return $self->invoice_terms if $self->invoice_terms;
3764 #check for a customer- specific override
3765 my $cust_main = $self->cust_main;
3766 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3768 #use configured default
3769 $conf->config('invoice_default_terms') || '';
3775 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3776 $duedate = $self->_date() + ( $1 * 86400 );
3783 $self->due_date ? time2str(shift, $self->due_date) : '';
3786 sub balance_due_msg {
3788 my $msg = $self->mt('Balance Due');
3789 return $msg unless $self->terms;
3790 if ( $self->due_date ) {
3791 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3792 $self->due_date2str($date_format);
3793 } elsif ( $self->terms ) {
3794 $msg .= ' - '. $self->terms;
3799 sub balance_due_date {
3801 my $conf = $self->conf;
3803 if ( $conf->exists('invoice_default_terms')
3804 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3805 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3810 sub credit_balance_msg {
3812 $self->mt('Credit Balance Remaining')
3815 =item invnum_date_pretty
3817 Returns a string with the invoice number and date, for example:
3818 "Invoice #54 (3/20/2008)"
3822 sub invnum_date_pretty {
3824 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3829 Returns a string with the date, for example: "3/20/2008"
3835 time2str($date_format, $self->_date);
3838 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3840 Generate section information for all items appearing on this invoice.
3841 This will only be called for multi-section invoices.
3843 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3844 related display records (L<FS::cust_bill_pkg_display>) and organize
3845 them into two groups ("early" and "late" according to whether they come
3846 before or after the total), then into sections. A subtotal is calculated
3849 Section descriptions are returned in sort weight order. Each consists
3850 of a hash containing:
3852 description: the package category name, escaped
3853 subtotal: the total charges in that section
3854 tax_section: a flag indicating that the section contains only tax charges
3855 summarized: same as tax_section, for some reason
3856 sort_weight: the package category's sort weight
3858 If 'condense' is set on the display record, it also contains everything
3859 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3860 coderefs to generate parts of the invoice. This is not advised.
3864 LATE: an arrayref to push the "late" section hashes onto. The "early"
3865 group is simply returned from the method.
3867 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3868 Turning this on has the following effects:
3869 - Ignores display items with the 'summary' flag.
3870 - Combines all items into the "early" group.
3871 - Creates sections for all non-disabled package categories, even if they
3872 have no charges on this invoice, as well as a section with no name.
3874 ESCAPE: an escape function to use for section titles.
3876 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3877 sorted list. If there are any of these, section subtotals exclude
3880 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3881 passed through to C<_condense_section()>.
3885 use vars qw(%pkg_category_cache);
3886 sub _items_sections {
3889 my $summarypage = shift;
3891 my $extra_sections = shift;
3895 my %late_subtotal = ();
3898 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3901 my $usage = $cust_bill_pkg->usage;
3903 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3904 next if ( $display->summary && $summarypage );
3906 my $section = $display->section;
3907 my $type = $display->type;
3909 $not_tax{$section} = 1
3910 unless $cust_bill_pkg->pkgnum == 0;
3912 if ( $display->post_total && !$summarypage ) {
3913 if (! $type || $type eq 'S') {
3914 $late_subtotal{$section} += $cust_bill_pkg->setup
3915 if $cust_bill_pkg->setup != 0
3916 || $cust_bill_pkg->setup_show_zero;
3920 $late_subtotal{$section} += $cust_bill_pkg->recur
3921 if $cust_bill_pkg->recur != 0
3922 || $cust_bill_pkg->recur_show_zero;
3925 if ($type && $type eq 'R') {
3926 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3927 if $cust_bill_pkg->recur != 0
3928 || $cust_bill_pkg->recur_show_zero;
3931 if ($type && $type eq 'U') {
3932 $late_subtotal{$section} += $usage
3933 unless scalar(@$extra_sections);
3938 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3940 if (! $type || $type eq 'S') {
3941 $subtotal{$section} += $cust_bill_pkg->setup
3942 if $cust_bill_pkg->setup != 0
3943 || $cust_bill_pkg->setup_show_zero;
3947 $subtotal{$section} += $cust_bill_pkg->recur
3948 if $cust_bill_pkg->recur != 0
3949 || $cust_bill_pkg->recur_show_zero;
3952 if ($type && $type eq 'R') {
3953 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3954 if $cust_bill_pkg->recur != 0
3955 || $cust_bill_pkg->recur_show_zero;
3958 if ($type && $type eq 'U') {
3959 $subtotal{$section} += $usage
3960 unless scalar(@$extra_sections);
3969 %pkg_category_cache = ();
3971 push @$late, map { { 'description' => &{$escape}($_),
3972 'subtotal' => $late_subtotal{$_},
3974 'sort_weight' => ( _pkg_category($_)
3975 ? _pkg_category($_)->weight
3978 ((_pkg_category($_) && _pkg_category($_)->condense)
3979 ? $self->_condense_section($format)
3983 sort _sectionsort keys %late_subtotal;
3986 if ( $summarypage ) {
3987 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3988 map { $_->categoryname } qsearch('pkg_category', {});
3989 push @sections, '' if exists($subtotal{''});
3991 @sections = keys %subtotal;
3994 my @early = map { { 'description' => &{$escape}($_),
3995 'subtotal' => $subtotal{$_},
3996 'summarized' => $not_tax{$_} ? '' : 'Y',
3997 'tax_section' => $not_tax{$_} ? '' : 'Y',
3998 'sort_weight' => ( _pkg_category($_)
3999 ? _pkg_category($_)->weight
4002 ((_pkg_category($_) && _pkg_category($_)->condense)
4003 ? $self->_condense_section($format)
4008 push @early, @$extra_sections if $extra_sections;
4010 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4014 #helper subs for above
4017 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4021 my $categoryname = shift;
4022 $pkg_category_cache{$categoryname} ||=
4023 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4026 my %condensed_format = (
4027 'label' => [ qw( Description Qty Amount ) ],
4029 sub { shift->{description} },
4030 sub { shift->{quantity} },
4031 sub { my($href, %opt) = @_;
4032 ($opt{dollar} || ''). $href->{amount};
4035 'align' => [ qw( l r r ) ],
4036 'span' => [ qw( 5 1 1 ) ], # unitprices?
4037 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4040 sub _condense_section {
4041 my ( $self, $format ) = ( shift, shift );
4043 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4044 qw( description_generator
4047 total_line_generator
4052 sub _condensed_generator_defaults {
4053 my ( $self, $format ) = ( shift, shift );
4054 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4063 sub _condensed_header_generator {
4064 my ( $self, $format ) = ( shift, shift );
4066 my ( $f, $prefix, $suffix, $separator, $column ) =
4067 _condensed_generator_defaults($format);
4069 if ($format eq 'latex') {
4070 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4071 $suffix = "\\\\\n\\hline";
4074 sub { my ($d,$a,$s,$w) = @_;
4075 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4077 } elsif ( $format eq 'html' ) {
4078 $prefix = '<th></th>';
4082 sub { my ($d,$a,$s,$w) = @_;
4083 return qq!<th align="$html_align{$a}">$d</th>!;
4091 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4093 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4096 $prefix. join($separator, @result). $suffix;
4101 sub _condensed_description_generator {
4102 my ( $self, $format ) = ( shift, shift );
4104 my ( $f, $prefix, $suffix, $separator, $column ) =
4105 _condensed_generator_defaults($format);
4107 my $money_char = '$';
4108 if ($format eq 'latex') {
4109 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4111 $separator = " & \n";
4113 sub { my ($d,$a,$s,$w) = @_;
4114 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4116 $money_char = '\\dollar';
4117 }elsif ( $format eq 'html' ) {
4118 $prefix = '"><td align="center"></td>';
4122 sub { my ($d,$a,$s,$w) = @_;
4123 return qq!<td align="$html_align{$a}">$d</td>!;
4125 #$money_char = $conf->config('money_char') || '$';
4126 $money_char = ''; # this is madness
4134 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4136 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4138 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4139 map { $f->{$_}->[$i] } qw(align span width)
4143 $prefix. join( $separator, @result ). $suffix;
4148 sub _condensed_total_generator {
4149 my ( $self, $format ) = ( shift, shift );
4151 my ( $f, $prefix, $suffix, $separator, $column ) =
4152 _condensed_generator_defaults($format);
4155 if ($format eq 'latex') {
4158 $separator = " & \n";
4160 sub { my ($d,$a,$s,$w) = @_;
4161 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4163 }elsif ( $format eq 'html' ) {
4167 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4169 sub { my ($d,$a,$s,$w) = @_;
4170 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4179 # my $r = &{$f->{fields}->[$i]}(@args);
4180 # $r .= ' Total' unless $i;
4182 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4184 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4185 map { $f->{$_}->[$i] } qw(align span width)
4189 $prefix. join( $separator, @result ). $suffix;
4194 =item total_line_generator FORMAT
4196 Returns a coderef used for generation of invoice total line items for this
4197 usage_class. FORMAT is either html or latex
4201 # should not be used: will have issues with hash element names (description vs
4202 # total_item and amount vs total_amount -- another array of functions?
4204 sub _condensed_total_line_generator {
4205 my ( $self, $format ) = ( shift, shift );
4207 my ( $f, $prefix, $suffix, $separator, $column ) =
4208 _condensed_generator_defaults($format);
4211 if ($format eq 'latex') {
4214 $separator = " & \n";
4216 sub { my ($d,$a,$s,$w) = @_;
4217 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4219 }elsif ( $format eq 'html' ) {
4223 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4225 sub { my ($d,$a,$s,$w) = @_;
4226 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4235 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4237 &{$column}( &{$f->{fields}->[$i]}(@args),
4238 map { $f->{$_}->[$i] } qw(align span width)
4242 $prefix. join( $separator, @result ). $suffix;
4247 #sub _items_extra_usage_sections {
4249 # my $escape = shift;
4251 # my %sections = ();
4253 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4254 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4256 # next unless $cust_bill_pkg->pkgnum > 0;
4258 # foreach my $section ( keys %usage_class ) {
4260 # my $usage = $cust_bill_pkg->usage($section);
4262 # next unless $usage && $usage > 0;
4264 # $sections{$section} ||= 0;
4265 # $sections{$section} += $usage;
4271 # map { { 'description' => &{$escape}($_),
4272 # 'subtotal' => $sections{$_},
4273 # 'summarized' => '',
4274 # 'tax_section' => '',
4277 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4281 sub _items_extra_usage_sections {
4283 my $conf = $self->conf;
4291 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4293 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4294 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4295 next unless $cust_bill_pkg->pkgnum > 0;
4297 foreach my $classnum ( keys %usage_class ) {
4298 my $section = $usage_class{$classnum}->classname;
4299 $classnums{$section} = $classnum;
4301 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4302 my $amount = $detail->amount;
4303 next unless $amount && $amount > 0;
4305 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4306 $sections{$section}{amount} += $amount; #subtotal
4307 $sections{$section}{calls}++;
4308 $sections{$section}{duration} += $detail->duration;
4310 my $desc = $detail->regionname;
4311 my $description = $desc;
4312 $description = substr($desc, 0, $maxlength). '...'
4313 if $format eq 'latex' && length($desc) > $maxlength;
4315 $lines{$section}{$desc} ||= {
4316 description => &{$escape}($description),
4317 #pkgpart => $part_pkg->pkgpart,
4318 pkgnum => $cust_bill_pkg->pkgnum,
4323 #unit_amount => $cust_bill_pkg->unitrecur,
4324 quantity => $cust_bill_pkg->quantity,
4325 product_code => 'N/A',
4326 ext_description => [],
4329 $lines{$section}{$desc}{amount} += $amount;
4330 $lines{$section}{$desc}{calls}++;
4331 $lines{$section}{$desc}{duration} += $detail->duration;
4337 my %sectionmap = ();
4338 foreach (keys %sections) {
4339 my $usage_class = $usage_class{$classnums{$_}};
4340 $sectionmap{$_} = { 'description' => &{$escape}($_),
4341 'amount' => $sections{$_}{amount}, #subtotal
4342 'calls' => $sections{$_}{calls},
4343 'duration' => $sections{$_}{duration},
4345 'tax_section' => '',
4346 'sort_weight' => $usage_class->weight,
4347 ( $usage_class->format
4348 ? ( map { $_ => $usage_class->$_($format) }
4349 qw( description_generator header_generator total_generator total_line_generator )
4356 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4360 foreach my $section ( keys %lines ) {
4361 foreach my $line ( keys %{$lines{$section}} ) {
4362 my $l = $lines{$section}{$line};
4363 $l->{section} = $sectionmap{$section};
4364 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4365 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4370 return(\@sections, \@lines);
4376 my $end = $self->_date;
4378 # start at date of previous invoice + 1 second or 0 if no previous invoice
4379 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4380 $start = 0 if !$start;
4383 my $cust_main = $self->cust_main;
4384 my @pkgs = $cust_main->all_pkgs;
4385 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4388 foreach my $pkg ( @pkgs ) {
4389 my @h_cust_svc = $pkg->h_cust_svc($end);
4390 foreach my $h_cust_svc ( @h_cust_svc ) {
4391 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4392 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4394 my $inserted = $h_cust_svc->date_inserted;
4395 my $deleted = $h_cust_svc->date_deleted;
4396 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4398 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4400 # DID either activated or ported in; cannot be both for same DID simultaneously
4401 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4402 && (!$phone_inserted->lnp_status
4403 || $phone_inserted->lnp_status eq ''
4404 || $phone_inserted->lnp_status eq 'native')) {
4407 else { # this one not so clean, should probably move to (h_)svc_phone
4408 my $phone_portedin = qsearchs( 'h_svc_phone',
4409 { 'svcnum' => $h_cust_svc->svcnum,
4410 'lnp_status' => 'portedin' },
4411 FS::h_svc_phone->sql_h_searchs($end),
4413 $num_portedin++ if $phone_portedin;
4416 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4417 if($deleted >= $start && $deleted <= $end && $phone_deleted
4418 && (!$phone_deleted->lnp_status
4419 || $phone_deleted->lnp_status ne 'portingout')) {
4422 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4423 && $phone_deleted->lnp_status
4424 && $phone_deleted->lnp_status eq 'portingout') {
4428 # increment usage minutes
4429 if ( $phone_inserted ) {
4430 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4431 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4434 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4437 # don't look at this service again
4438 push @seen, $h_cust_svc->svcnum;
4442 $minutes = sprintf("%d", $minutes);
4443 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4444 . "$num_deactivated Ported-Out: $num_portedout ",
4445 "Total Minutes: $minutes");
4448 sub _items_accountcode_cdr {
4453 my $section = { 'amount' => 0,
4456 'sort_weight' => '',
4458 'description' => 'Usage by Account Code',
4464 my %accountcodes = ();
4466 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4467 next unless $cust_bill_pkg->pkgnum > 0;
4469 my @header = $cust_bill_pkg->details_header;
4470 next unless scalar(@header);
4471 $section->{'header'} = join(',',@header);
4473 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4475 $section->{'header'} = $detail->formatted('format' => $format)
4476 if($detail->detail eq $section->{'header'});
4478 my $accountcode = $detail->accountcode;
4479 next unless $accountcode;
4481 my $amount = $detail->amount;
4482 next unless $amount && $amount > 0;
4484 $accountcodes{$accountcode} ||= {
4485 description => $accountcode,
4492 product_code => 'N/A',
4493 section => $section,
4494 ext_description => [ $section->{'header'} ],
4498 $section->{'amount'} += $amount;
4499 $accountcodes{$accountcode}{'amount'} += $amount;
4500 $accountcodes{$accountcode}{calls}++;
4501 $accountcodes{$accountcode}{duration} += $detail->duration;
4502 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4506 foreach my $l ( values %accountcodes ) {
4507 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4508 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4509 foreach my $sorted_detail ( @sorted_detail ) {
4510 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4512 delete $l->{detail_temp};
4516 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4518 return ($section,\@sorted_lines);
4521 sub _items_svc_phone_sections {
4523 my $conf = $self->conf;
4531 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4533 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4534 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4536 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4537 next unless $cust_bill_pkg->pkgnum > 0;
4539 my @header = $cust_bill_pkg->details_header;
4540 next unless scalar(@header);
4542 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4544 my $phonenum = $detail->phonenum;
4545 next unless $phonenum;
4547 my $amount = $detail->amount;
4548 next unless $amount && $amount > 0;
4550 $sections{$phonenum} ||= { 'amount' => 0,
4553 'sort_weight' => -1,
4554 'phonenum' => $phonenum,
4556 $sections{$phonenum}{amount} += $amount; #subtotal
4557 $sections{$phonenum}{calls}++;
4558 $sections{$phonenum}{duration} += $detail->duration;
4560 my $desc = $detail->regionname;
4561 my $description = $desc;
4562 $description = substr($desc, 0, $maxlength). '...'
4563 if $format eq 'latex' && length($desc) > $maxlength;
4565 $lines{$phonenum}{$desc} ||= {
4566 description => &{$escape}($description),
4567 #pkgpart => $part_pkg->pkgpart,
4575 product_code => 'N/A',
4576 ext_description => [],
4579 $lines{$phonenum}{$desc}{amount} += $amount;
4580 $lines{$phonenum}{$desc}{calls}++;
4581 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4583 my $line = $usage_class{$detail->classnum}->classname;
4584 $sections{"$phonenum $line"} ||=
4588 'sort_weight' => $usage_class{$detail->classnum}->weight,
4589 'phonenum' => $phonenum,
4590 'header' => [ @header ],
4592 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4593 $sections{"$phonenum $line"}{calls}++;
4594 $sections{"$phonenum $line"}{duration} += $detail->duration;
4596 $lines{"$phonenum $line"}{$desc} ||= {
4597 description => &{$escape}($description),
4598 #pkgpart => $part_pkg->pkgpart,
4606 product_code => 'N/A',
4607 ext_description => [],
4610 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4611 $lines{"$phonenum $line"}{$desc}{calls}++;
4612 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4613 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4614 $detail->formatted('format' => $format);
4619 my %sectionmap = ();
4620 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4621 foreach ( keys %sections ) {
4622 my @header = @{ $sections{$_}{header} || [] };
4624 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4625 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4626 my $usage_class = $summary ? $simple : $usage_simple;
4627 my $ending = $summary ? ' usage charges' : '';
4630 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4632 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4633 'amount' => $sections{$_}{amount}, #subtotal
4634 'calls' => $sections{$_}{calls},
4635 'duration' => $sections{$_}{duration},
4637 'tax_section' => '',
4638 'phonenum' => $sections{$_}{phonenum},
4639 'sort_weight' => $sections{$_}{sort_weight},
4640 'post_total' => $summary, #inspire pagebreak
4642 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4643 qw( description_generator
4646 total_line_generator
4653 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4654 $a->{sort_weight} <=> $b->{sort_weight}
4659 foreach my $section ( keys %lines ) {
4660 foreach my $line ( keys %{$lines{$section}} ) {
4661 my $l = $lines{$section}{$line};
4662 $l->{section} = $sectionmap{$section};
4663 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4664 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4669 if($conf->exists('phone_usage_class_summary')) {
4670 # this only works with Latex
4674 # after this, we'll have only two sections per DID:
4675 # Calls Summary and Calls Detail
4676 foreach my $section ( @sections ) {
4677 if($section->{'post_total'}) {
4678 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4679 $section->{'total_line_generator'} = sub { '' };
4680 $section->{'total_generator'} = sub { '' };
4681 $section->{'header_generator'} = sub { '' };
4682 $section->{'description_generator'} = '';
4683 push @newsections, $section;
4684 my %calls_detail = %$section;
4685 $calls_detail{'post_total'} = '';
4686 $calls_detail{'sort_weight'} = '';
4687 $calls_detail{'description_generator'} = sub { '' };
4688 $calls_detail{'header_generator'} = sub {
4689 return ' & Date/Time & Called Number & Duration & Price'
4690 if $format eq 'latex';
4693 $calls_detail{'description'} = 'Calls Detail: '
4694 . $section->{'phonenum'};
4695 push @newsections, \%calls_detail;
4699 # after this, each usage class is collapsed/summarized into a single
4700 # line under the Calls Summary section
4701 foreach my $newsection ( @newsections ) {
4702 if($newsection->{'post_total'}) { # this means Calls Summary
4703 foreach my $section ( @sections ) {
4704 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4705 && !$section->{'post_total'});
4706 my $newdesc = $section->{'description'};
4707 my $tn = $section->{'phonenum'};
4708 $newdesc =~ s/$tn//g;
4709 my $line = { ext_description => [],
4713 calls => $section->{'calls'},
4714 section => $newsection,
4715 duration => $section->{'duration'},
4716 description => $newdesc,
4717 amount => sprintf("%.2f",$section->{'amount'}),
4718 product_code => 'N/A',
4720 push @newlines, $line;
4725 # after this, Calls Details is populated with all CDRs
4726 foreach my $newsection ( @newsections ) {
4727 if(!$newsection->{'post_total'}) { # this means Calls Details
4728 foreach my $line ( @lines ) {
4729 next unless (scalar(@{$line->{'ext_description'}}) &&
4730 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4732 my @extdesc = @{$line->{'ext_description'}};
4734 foreach my $extdesc ( @extdesc ) {
4735 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4736 push @newextdesc, $extdesc;
4738 $line->{'ext_description'} = \@newextdesc;
4739 $line->{'section'} = $newsection;
4740 push @newlines, $line;
4745 return(\@newsections, \@newlines);
4748 return(\@sections, \@lines);
4752 sub _items { # seems to be unused
4755 #my @display = scalar(@_)
4757 # : qw( _items_previous _items_pkg );
4758 # #: qw( _items_pkg );
4759 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4760 my @display = qw( _items_previous _items_pkg );
4763 foreach my $display ( @display ) {
4764 push @b, $self->$display(@_);
4769 sub _items_previous {
4771 my $conf = $self->conf;
4772 my $cust_main = $self->cust_main;
4773 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4775 foreach ( @pr_cust_bill ) {
4776 my $date = $conf->exists('invoice_show_prior_due_date')
4777 ? 'due '. $_->due_date2str($date_format)
4778 : time2str($date_format, $_->_date);
4780 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4781 #'pkgpart' => 'N/A',
4783 'amount' => sprintf("%.2f", $_->owed),
4789 # 'description' => 'Previous Balance',
4790 # #'pkgpart' => 'N/A',
4791 # 'pkgnum' => 'N/A',
4792 # 'amount' => sprintf("%10.2f", $pr_total ),
4793 # 'ext_description' => [ map {
4794 # "Invoice ". $_->invnum.
4795 # " (". time2str("%x",$_->_date). ") ".
4796 # sprintf("%10.2f", $_->owed)
4797 # } @pr_cust_bill ],
4802 =item _items_pkg [ OPTIONS ]
4804 Return line item hashes for each package item on this invoice. Nearly
4807 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4809 The only OPTIONS accepted is 'section', which may point to a hashref
4810 with a key named 'condensed', which may have a true value. If it
4811 does, this method tries to merge identical items into items with
4812 'quantity' equal to the number of items (not the sum of their
4813 separate quantities, for some reason).
4821 warn "$me _items_pkg searching for all package line items\n"
4824 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4826 warn "$me _items_pkg filtering line items\n"
4828 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4830 if ($options{section} && $options{section}->{condensed}) {
4832 warn "$me _items_pkg condensing section\n"
4836 local $Storable::canonical = 1;
4837 foreach ( @items ) {
4839 delete $item->{ref};
4840 delete $item->{ext_description};
4841 my $key = freeze($item);
4842 $itemshash{$key} ||= 0;
4843 $itemshash{$key} ++; # += $item->{quantity};
4845 @items = sort { $a->{description} cmp $b->{description} }
4846 map { my $i = thaw($_);
4847 $i->{quantity} = $itemshash{$_};
4849 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4855 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4862 return 0 unless $a->itemdesc cmp $b->itemdesc;
4863 return -1 if $b->itemdesc eq 'Tax';
4864 return 1 if $a->itemdesc eq 'Tax';
4865 return -1 if $b->itemdesc eq 'Other surcharges';
4866 return 1 if $a->itemdesc eq 'Other surcharges';
4867 $a->itemdesc cmp $b->itemdesc;
4872 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4873 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4876 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4878 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4879 list of hashrefs describing the line items they generate on the invoice.
4881 OPTIONS may include:
4883 format: the invoice format.
4885 escape_function: the function used to escape strings.
4887 DEPRECATED? (expensive, mostly unused?)
4888 format_function: the function used to format CDRs.
4890 section: a hashref containing 'description'; if this is present,
4891 cust_bill_pkg_display records not belonging to this section are
4894 multisection: a flag indicating that this is a multisection invoice,
4895 which does something complicated.
4897 multilocation: a flag to display the location label for the package.
4899 Returns a list of hashrefs, each of which may contain:
4901 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4902 ext_description, which is an arrayref of detail lines to show below
4907 sub _items_cust_bill_pkg {
4909 my $conf = $self->conf;
4910 my $cust_bill_pkgs = shift;
4913 my $format = $opt{format} || '';
4914 my $escape_function = $opt{escape_function} || sub { shift };
4915 my $format_function = $opt{format_function} || '';
4916 my $no_usage = $opt{no_usage} || '';
4917 my $unsquelched = $opt{unsquelched} || ''; #unused
4918 my $section = $opt{section}->{description} if $opt{section};
4919 my $summary_page = $opt{summary_page} || ''; #unused
4920 my $multilocation = $opt{multilocation} || '';
4921 my $multisection = $opt{multisection} || '';
4922 my $discount_show_always = 0;
4924 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4926 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4929 my ($s, $r, $u) = ( undef, undef, undef );
4930 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4933 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4934 if ( $_ && !$cust_bill_pkg->hidden ) {
4935 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4936 $_->{amount} =~ s/^\-0\.00$/0.00/;
4937 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4939 if $_->{amount} != 0
4940 || $discount_show_always
4941 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4942 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4948 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4950 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4951 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4954 foreach my $display ( grep { defined($section)
4955 ? $_->section eq $section
4958 #grep { !$_->summary || !$summary_page } # bunk!
4959 grep { !$_->summary || $multisection }
4960 @cust_bill_pkg_display
4964 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4965 $display->billpkgdisplaynum. "\n"
4968 my $type = $display->type;
4970 my $desc = $cust_bill_pkg->desc;
4971 $desc = substr($desc, 0, $maxlength). '...'
4972 if $format eq 'latex' && length($desc) > $maxlength;
4974 my %details_opt = ( 'format' => $format,
4975 'escape_function' => $escape_function,
4976 'format_function' => $format_function,
4977 'no_usage' => $opt{'no_usage'},
4980 if ( $cust_bill_pkg->pkgnum > 0 ) {
4982 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4985 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4987 # start/end dates for invoice formats that do nonstandard
4989 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4991 if ( (!$type || $type eq 'S')
4992 && ( $cust_bill_pkg->setup != 0
4993 || $cust_bill_pkg->setup_show_zero
4998 warn "$me _items_cust_bill_pkg adding setup\n"
5001 my $description = $desc;
5002 $description .= ' Setup'
5003 if $cust_bill_pkg->recur != 0
5004 || $discount_show_always
5005 || $cust_bill_pkg->recur_show_zero;
5008 unless ( $cust_pkg->part_pkg->hide_svc_detail
5009 || $cust_bill_pkg->hidden )
5012 push @d, map &{$escape_function}($_),
5013 $cust_pkg->h_labels_short($self->_date, undef, 'I')
5014 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5016 if ( $multilocation ) {
5017 my $loc = $cust_pkg->location_label;
5018 $loc = substr($loc, 0, $maxlength). '...'
5019 if $format eq 'latex' && length($loc) > $maxlength;
5020 push @d, &{$escape_function}($loc);
5023 } #unless hiding service details
5025 push @d, $cust_bill_pkg->details(%details_opt)
5026 if $cust_bill_pkg->recur == 0;
5028 if ( $cust_bill_pkg->hidden ) {
5029 $s->{amount} += $cust_bill_pkg->setup;
5030 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5031 push @{ $s->{ext_description} }, @d;
5035 description => $description,
5036 #pkgpart => $part_pkg->pkgpart,
5037 pkgnum => $cust_bill_pkg->pkgnum,
5038 amount => $cust_bill_pkg->setup,
5039 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5040 unit_amount => $cust_bill_pkg->unitsetup,
5041 quantity => $cust_bill_pkg->quantity,
5042 ext_description => \@d,
5048 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5050 $cust_bill_pkg->recur != 0
5051 || $cust_bill_pkg->setup == 0
5052 || $discount_show_always
5053 || $cust_bill_pkg->recur_show_zero
5058 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5061 my $is_summary = $display->summary;
5062 my $description = ($is_summary && $type && $type eq 'U')
5063 ? "Usage charges" : $desc;
5065 #pry be a bit more efficient to look some of this conf stuff up
5068 $conf->exists('disable_line_item_date_ranges')
5069 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5072 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5073 $cust_main->agentnum
5075 if ( defined($date_style) && $date_style eq 'month_of' ) {
5076 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5077 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5078 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5079 $cust_main->agentnum
5081 $desc .= ' ' unless $desc =~ /\s$/;
5082 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5084 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5085 " - ". time2str($date_format, $cust_bill_pkg->edate);
5087 $description .= " ($time_period)";
5091 my @seconds = (); # for display of usage info
5093 #at least until cust_bill_pkg has "past" ranges in addition to
5094 #the "future" sdate/edate ones... see #3032
5095 my @dates = ( $self->_date );
5096 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5097 push @dates, $prev->sdate if $prev;
5098 push @dates, undef if !$prev;
5100 unless ( $cust_pkg->part_pkg->hide_svc_detail
5101 || $cust_bill_pkg->itemdesc
5102 || $cust_bill_pkg->hidden
5103 || $is_summary && $type && $type eq 'U' )
5106 warn "$me _items_cust_bill_pkg adding service details\n"
5109 push @d, map &{$escape_function}($_),
5110 $cust_pkg->h_labels_short(@dates, 'I')
5111 #$cust_bill_pkg->edate,
5112 #$cust_bill_pkg->sdate)
5113 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5115 warn "$me _items_cust_bill_pkg done adding service details\n"
5118 if ( $multilocation ) {
5119 my $loc = $cust_pkg->location_label;
5120 $loc = substr($loc, 0, $maxlength). '...'
5121 if $format eq 'latex' && length($loc) > $maxlength;
5122 push @d, &{$escape_function}($loc);
5125 # Display of seconds_since_sqlradacct:
5126 # On the invoice, when processing @detail_items, look for a field
5127 # named 'seconds'. This will contain total seconds for each
5128 # service, in the same order as @ext_description. For services
5129 # that don't support this it will show undef.
5130 if ( $conf->exists('svc_acct-usage_seconds')
5131 and ! $cust_bill_pkg->pkgpart_override ) {
5132 foreach my $cust_svc (
5133 $cust_pkg->h_cust_svc(@dates, 'I')
5136 # eval because not having any part_export_usage exports
5137 # is a fatal error, last_bill/_date because that's how
5138 # sqlradius_hour billing does it
5140 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5142 push @seconds, $sec;
5144 } #if svc_acct-usage_seconds
5148 unless ( $is_summary ) {
5149 warn "$me _items_cust_bill_pkg adding details\n"
5152 #instead of omitting details entirely in this case (unwanted side
5153 # effects), just omit CDRs
5154 $details_opt{'no_usage'} = 1
5155 if $type && $type eq 'R';
5157 push @d, $cust_bill_pkg->details(%details_opt);
5160 warn "$me _items_cust_bill_pkg calculating amount\n"
5165 $amount = $cust_bill_pkg->recur;
5166 } elsif ($type eq 'R') {
5167 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5168 } elsif ($type eq 'U') {
5169 $amount = $cust_bill_pkg->usage;
5172 if ( !$type || $type eq 'R' ) {
5174 warn "$me _items_cust_bill_pkg adding recur\n"
5177 if ( $cust_bill_pkg->hidden ) {
5178 $r->{amount} += $amount;
5179 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5180 push @{ $r->{ext_description} }, @d;
5183 description => $description,
5184 #pkgpart => $part_pkg->pkgpart,
5185 pkgnum => $cust_bill_pkg->pkgnum,
5187 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5188 unit_amount => $cust_bill_pkg->unitrecur,
5189 quantity => $cust_bill_pkg->quantity,
5191 ext_description => \@d,
5193 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5196 } else { # $type eq 'U'
5198 warn "$me _items_cust_bill_pkg adding usage\n"
5201 if ( $cust_bill_pkg->hidden ) {
5202 $u->{amount} += $amount;
5203 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5204 push @{ $u->{ext_description} }, @d;
5207 description => $description,
5208 #pkgpart => $part_pkg->pkgpart,
5209 pkgnum => $cust_bill_pkg->pkgnum,
5211 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5212 unit_amount => $cust_bill_pkg->unitrecur,
5213 quantity => $cust_bill_pkg->quantity,
5215 ext_description => \@d,
5220 } # recurring or usage with recurring charge
5222 } else { #pkgnum tax or one-shot line item (??)
5224 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5227 if ( $cust_bill_pkg->setup != 0 ) {
5229 'description' => $desc,
5230 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5233 if ( $cust_bill_pkg->recur != 0 ) {
5235 'description' => "$desc (".
5236 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5237 time2str($date_format, $cust_bill_pkg->edate). ')',
5238 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5246 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5247 && $conf->exists('discount-show-always'));
5251 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5253 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5254 $_->{amount} =~ s/^\-0\.00$/0.00/;
5255 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5257 if $_->{amount} != 0
5258 || $discount_show_always
5259 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5260 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5264 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5271 sub _items_credits {
5272 my( $self, %opt ) = @_;
5273 my $trim_len = $opt{'trim_len'} || 60;
5277 foreach ( $self->cust_credited ) {
5279 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5281 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5282 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5283 $reason = " ($reason) " if $reason;
5286 #'description' => 'Credit ref\#'. $_->crednum.
5287 # " (". time2str("%x",$_->cust_credit->_date) .")".
5289 'description' => $self->mt('Credit applied').' '.
5290 time2str($date_format,$_->cust_credit->_date). $reason,
5291 'amount' => sprintf("%.2f",$_->amount),
5299 sub _items_payments {
5303 #get & print payments
5304 foreach ( $self->cust_bill_pay ) {
5306 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5309 'description' => $self->mt('Payment received').' '.
5310 time2str($date_format,$_->cust_pay->_date ),
5311 'amount' => sprintf("%.2f", $_->amount )
5319 =item _items_discounts_avail
5321 Returns an array of line item hashrefs representing available term discounts
5322 for this invoice. This makes the same assumptions that apply to term
5323 discounts in general: that the package is billed monthly, at a flat rate,
5324 with no usage charges. A prorated first month will be handled, as will
5325 a setup fee if the discount is allowed to apply to setup fees.
5329 sub _items_discounts_avail {
5331 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5333 my %plans = $self->discount_plans;
5335 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5339 my $plan = $plans{$months};
5341 my $term_total = sprintf('%.2f', $plan->discounted_total);
5342 my $percent = sprintf('%.0f',
5343 100 * (1 - $term_total / $plan->base_total) );
5344 my $permonth = sprintf('%.2f', $term_total / $months);
5345 my $detail = $self->mt('discount on item'). ' '.
5346 join(', ', map { "#$_" } $plan->pkgnums)
5349 # discounts for non-integer months don't work anyway
5350 $months = sprintf("%d", $months);
5353 description => $self->mt('Save [_1]% by paying for [_2] months',
5355 amount => $self->mt('[_1] ([_2] per month)',
5356 $term_total, $money_char.$permonth),
5357 ext_description => ($detail || ''),
5360 sort { $b <=> $a } keys %plans;
5364 =item call_details [ OPTION => VALUE ... ]
5366 Returns an array of CSV strings representing the call details for this invoice
5367 The only option available is the boolean prepend_billed_number
5372 my ($self, %opt) = @_;
5374 my $format_function = sub { shift };
5376 if ($opt{prepend_billed_number}) {
5377 $format_function = sub {
5381 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5386 my @details = map { $_->details( 'format_function' => $format_function,
5387 'escape_function' => sub{ return() },
5391 $self->cust_bill_pkg;
5392 my $header = $details[0];
5393 ( $header, grep { $_ ne $header } @details );
5403 =item process_reprint
5407 sub process_reprint {
5408 process_re_X('print', @_);
5411 =item process_reemail
5415 sub process_reemail {
5416 process_re_X('email', @_);
5424 process_re_X('fax', @_);
5432 process_re_X('ftp', @_);
5439 sub process_respool {
5440 process_re_X('spool', @_);
5443 use Storable qw(thaw);
5447 my( $method, $job ) = ( shift, shift );
5448 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5450 my $param = thaw(decode_base64(shift));
5451 warn Dumper($param) if $DEBUG;
5462 my($method, $job, %param ) = @_;
5464 warn "re_X $method for job $job with param:\n".
5465 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5468 #some false laziness w/search/cust_bill.html
5470 my $orderby = 'ORDER BY cust_bill._date';
5472 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5474 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5476 my @cust_bill = qsearch( {
5477 #'select' => "cust_bill.*",
5478 'table' => 'cust_bill',
5479 'addl_from' => $addl_from,
5481 'extra_sql' => $extra_sql,
5482 'order_by' => $orderby,
5486 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5488 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5491 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5492 foreach my $cust_bill ( @cust_bill ) {
5493 $cust_bill->$method();
5495 if ( $job ) { #progressbar foo
5497 if ( time - $min_sec > $last ) {
5498 my $error = $job->update_statustext(
5499 int( 100 * $num / scalar(@cust_bill) )
5501 die $error if $error;
5512 =head1 CLASS METHODS
5518 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5523 my ($class, $start, $end) = @_;
5525 $class->paid_sql($start, $end). ' - '.
5526 $class->credited_sql($start, $end);
5531 Returns an SQL fragment to retreive the net amount (charged minus credited).
5536 my ($class, $start, $end) = @_;
5537 'charged - '. $class->credited_sql($start, $end);
5542 Returns an SQL fragment to retreive the amount paid against this invoice.
5547 my ($class, $start, $end) = @_;
5548 $start &&= "AND cust_bill_pay._date <= $start";
5549 $end &&= "AND cust_bill_pay._date > $end";
5550 $start = '' unless defined($start);
5551 $end = '' unless defined($end);
5552 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5553 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5558 Returns an SQL fragment to retreive the amount credited against this invoice.
5563 my ($class, $start, $end) = @_;
5564 $start &&= "AND cust_credit_bill._date <= $start";
5565 $end &&= "AND cust_credit_bill._date > $end";
5566 $start = '' unless defined($start);
5567 $end = '' unless defined($end);
5568 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5569 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5574 Returns an SQL fragment to retrieve the due date of an invoice.
5575 Currently only supported on PostgreSQL.
5580 my $conf = new FS::Conf;
5584 cust_bill.invoice_terms,
5585 cust_main.invoice_terms,
5586 \''.($conf->config('invoice_default_terms') || '').'\'
5587 ), E\'Net (\\\\d+)\'
5589 ) * 86400 + cust_bill._date'
5592 =item search_sql_where HASHREF
5594 Class method which returns an SQL WHERE fragment to search for parameters
5595 specified in HASHREF. Valid parameters are
5601 List reference of start date, end date, as UNIX timestamps.
5611 List reference of charged limits (exclusive).
5615 List reference of charged limits (exclusive).
5619 flag, return open invoices only
5623 flag, return net invoices only
5627 =item newest_percust
5631 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5635 sub search_sql_where {
5636 my($class, $param) = @_;
5638 warn "$me search_sql_where called with params: \n".
5639 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5645 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5646 push @search, "cust_main.agentnum = $1";
5650 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5651 push @search, "cust_main.refnum = $1";
5655 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5656 push @search, "cust_bill.custnum = $1";
5660 if ( $param->{_date} ) {
5661 my($beginning, $ending) = @{$param->{_date}};
5663 push @search, "cust_bill._date >= $beginning",
5664 "cust_bill._date < $ending";
5668 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5669 push @search, "cust_bill.invnum >= $1";
5671 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5672 push @search, "cust_bill.invnum <= $1";
5676 if ( $param->{charged} ) {
5677 my @charged = ref($param->{charged})
5678 ? @{ $param->{charged} }
5679 : ($param->{charged});
5681 push @search, map { s/^charged/cust_bill.charged/; $_; }
5685 my $owed_sql = FS::cust_bill->owed_sql;
5688 if ( $param->{owed} ) {
5689 my @owed = ref($param->{owed})
5690 ? @{ $param->{owed} }
5692 push @search, map { s/^owed/$owed_sql/; $_; }
5697 push @search, "0 != $owed_sql"
5698 if $param->{'open'};
5699 push @search, '0 != '. FS::cust_bill->net_sql
5703 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5704 if $param->{'days'};
5707 if ( $param->{'newest_percust'} ) {
5709 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5710 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5712 my @newest_where = map { my $x = $_;
5713 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5716 grep ! /^cust_main./, @search;
5717 my $newest_where = scalar(@newest_where)
5718 ? ' AND '. join(' AND ', @newest_where)
5722 push @search, "cust_bill._date = (
5723 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5724 WHERE newest_cust_bill.custnum = cust_bill.custnum
5730 #promised_date - also has an option to accept nulls
5731 if ( $param->{promised_date} ) {
5732 my($beginning, $ending, $null) = @{$param->{promised_date}};
5734 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5735 "cust_bill.promised_date < $ending )" .
5736 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5739 #agent virtualization
5740 my $curuser = $FS::CurrentUser::CurrentUser;
5741 if ( $curuser->username eq 'fs_queue'
5742 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5744 my $newuser = qsearchs('access_user', {
5745 'username' => $username,
5749 $curuser = $newuser;
5751 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5754 push @search, $curuser->agentnums_sql;
5756 join(' AND ', @search );
5768 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5769 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base