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 the customer's last invoice before this one.
388 if ( !$self->get('previous_bill') ) {
389 $self->set('previous_bill', qsearchs({
390 'table' => 'cust_bill',
391 'hashref' => { 'custnum' => $self->custnum,
392 '_date' => { op=>'<', value=>$self->_date } },
393 'order_by' => 'ORDER BY _date DESC LIMIT 1',
396 $self->get('previous_bill');
401 Returns a list consisting of the total previous balance for this customer,
402 followed by the previous outstanding invoices (as FS::cust_bill objects also).
409 my @cust_bill = sort { $a->_date <=> $b->_date }
410 grep { $_->owed != 0 }
411 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
412 #'_date' => { op=>'<', value=>$self->_date },
413 'invnum' => { op=>'<', value=>$self->invnum },
416 foreach ( @cust_bill ) { $total += $_->owed; }
420 =item enable_previous
422 Whether to show the 'Previous Charges' section when printing this invoice.
423 The negation of the 'disable_previous_balance' config setting.
427 sub enable_previous {
429 my $agentnum = $self->cust_main->agentnum;
430 !$self->conf->exists('disable_previous_balance', $agentnum);
435 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
442 { 'table' => 'cust_bill_pkg',
443 'hashref' => { 'invnum' => $self->invnum },
444 'order_by' => 'ORDER BY billpkgnum',
449 =item cust_bill_pkg_pkgnum PKGNUM
451 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
456 sub cust_bill_pkg_pkgnum {
457 my( $self, $pkgnum ) = @_;
459 { 'table' => 'cust_bill_pkg',
460 'hashref' => { 'invnum' => $self->invnum,
463 'order_by' => 'ORDER BY billpkgnum',
470 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
477 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
478 $self->cust_bill_pkg;
480 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
485 Returns true if any of the packages (or their definitions) corresponding to the
486 line items for this invoice have the no_auto flag set.
492 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
495 =item open_cust_bill_pkg
497 Returns the open line items for this invoice.
499 Note that cust_bill_pkg with both setup and recur fees are returned as two
500 separate line items, each with only one fee.
504 # modeled after cust_main::open_cust_bill
505 sub open_cust_bill_pkg {
508 # grep { $_->owed > 0 } $self->cust_bill_pkg
510 my %other = ( 'recur' => 'setup',
511 'setup' => 'recur', );
513 foreach my $field ( qw( recur setup )) {
514 push @open, map { $_->set( $other{$field}, 0 ); $_; }
515 grep { $_->owed($field) > 0 }
516 $self->cust_bill_pkg;
522 =item cust_bill_event
524 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
528 sub cust_bill_event {
530 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
533 =item num_cust_bill_event
535 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
539 sub num_cust_bill_event {
542 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
543 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
544 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
545 $sth->fetchrow_arrayref->[0];
550 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
554 #false laziness w/cust_pkg.pm
558 'table' => 'cust_event',
559 'addl_from' => 'JOIN part_event USING ( eventpart )',
560 'hashref' => { 'tablenum' => $self->invnum },
561 'extra_sql' => " AND eventtable = 'cust_bill' ",
567 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
571 #false laziness w/cust_pkg.pm
575 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
576 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
577 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
578 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
579 $sth->fetchrow_arrayref->[0];
584 Returns the customer (see L<FS::cust_main>) for this invoice.
590 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
593 =item cust_suspend_if_balance_over AMOUNT
595 Suspends the customer associated with this invoice if the total amount owed on
596 this invoice and all older invoices is greater than the specified amount.
598 Returns a list: an empty list on success or a list of errors.
602 sub cust_suspend_if_balance_over {
603 my( $self, $amount ) = ( shift, shift );
604 my $cust_main = $self->cust_main;
605 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
608 $cust_main->suspend(@_);
614 Depreciated. See the cust_credited method.
616 #Returns a list consisting of the total previous credited (see
617 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
618 #outstanding credits (FS::cust_credit objects).
624 croak "FS::cust_bill->cust_credit depreciated; see ".
625 "FS::cust_bill->cust_credit_bill";
628 #my @cust_credit = sort { $a->_date <=> $b->_date }
629 # grep { $_->credited != 0 && $_->_date < $self->_date }
630 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
632 #foreach (@cust_credit) { $total += $_->credited; }
633 #$total, @cust_credit;
638 Depreciated. See the cust_bill_pay method.
640 #Returns all payments (see L<FS::cust_pay>) for this invoice.
646 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
648 #sort { $a->_date <=> $b->_date }
649 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
655 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
658 sub cust_bill_pay_batch {
660 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
665 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
671 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
672 sort { $a->_date <=> $b->_date }
673 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
678 =item cust_credit_bill
680 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
686 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
687 sort { $a->_date <=> $b->_date }
688 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
692 sub cust_credit_bill {
693 shift->cust_credited(@_);
696 #=item cust_bill_pay_pkgnum PKGNUM
698 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
699 #with matching pkgnum.
703 #sub cust_bill_pay_pkgnum {
704 # my( $self, $pkgnum ) = @_;
705 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
706 # sort { $a->_date <=> $b->_date }
707 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
708 # 'pkgnum' => $pkgnum,
713 =item cust_bill_pay_pkg PKGNUM
715 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
716 applied against the matching pkgnum.
720 sub cust_bill_pay_pkg {
721 my( $self, $pkgnum ) = @_;
724 'select' => 'cust_bill_pay_pkg.*',
725 'table' => 'cust_bill_pay_pkg',
726 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
727 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
728 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
729 " AND cust_bill_pkg.pkgnum = $pkgnum",
734 #=item cust_credited_pkgnum PKGNUM
736 #=item cust_credit_bill_pkgnum PKGNUM
738 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
739 #with matching pkgnum.
743 #sub cust_credited_pkgnum {
744 # my( $self, $pkgnum ) = @_;
745 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
746 # sort { $a->_date <=> $b->_date }
747 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
748 # 'pkgnum' => $pkgnum,
753 #sub cust_credit_bill_pkgnum {
754 # shift->cust_credited_pkgnum(@_);
757 =item cust_credit_bill_pkg PKGNUM
759 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
760 applied against the matching pkgnum.
764 sub cust_credit_bill_pkg {
765 my( $self, $pkgnum ) = @_;
768 'select' => 'cust_credit_bill_pkg.*',
769 'table' => 'cust_credit_bill_pkg',
770 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
771 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
772 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
773 " AND cust_bill_pkg.pkgnum = $pkgnum",
778 =item cust_bill_batch
780 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
784 sub cust_bill_batch {
786 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
791 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
792 hash keyed by term length.
798 FS::discount_plan->all($self);
803 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
810 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
812 foreach (@taxlines) { $total += $_->setup; }
818 Returns the amount owed (still outstanding) on this invoice, which is charged
819 minus all payment applications (see L<FS::cust_bill_pay>) and credit
820 applications (see L<FS::cust_credit_bill>).
826 my $balance = $self->charged;
827 $balance -= $_->amount foreach ( $self->cust_bill_pay );
828 $balance -= $_->amount foreach ( $self->cust_credited );
829 $balance = sprintf( "%.2f", $balance);
830 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
835 my( $self, $pkgnum ) = @_;
837 #my $balance = $self->charged;
839 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
841 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
842 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
844 $balance = sprintf( "%.2f", $balance);
845 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
851 Returns true if this invoice should be hidden. See the
852 selfservice-hide_invoices-taxclass configuraiton setting.
858 my $conf = $self->conf;
859 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
861 my @cust_bill_pkg = $self->cust_bill_pkg;
862 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
863 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
866 =item apply_payments_and_credits [ OPTION => VALUE ... ]
868 Applies unapplied payments and credits to this invoice.
870 A hash of optional arguments may be passed. Currently "manual" is supported.
871 If true, a payment receipt is sent instead of a statement when
872 'payment_receipt_email' configuration option is set.
874 If there is an error, returns the error, otherwise returns false.
878 sub apply_payments_and_credits {
879 my( $self, %options ) = @_;
880 my $conf = $self->conf;
882 local $SIG{HUP} = 'IGNORE';
883 local $SIG{INT} = 'IGNORE';
884 local $SIG{QUIT} = 'IGNORE';
885 local $SIG{TERM} = 'IGNORE';
886 local $SIG{TSTP} = 'IGNORE';
887 local $SIG{PIPE} = 'IGNORE';
889 my $oldAutoCommit = $FS::UID::AutoCommit;
890 local $FS::UID::AutoCommit = 0;
893 $self->select_for_update; #mutex
895 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
896 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
898 if ( $conf->exists('pkg-balances') ) {
899 # limit @payments & @credits to those w/ a pkgnum grepped from $self
900 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
901 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
902 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
905 while ( $self->owed > 0 and ( @payments || @credits ) ) {
908 if ( @payments && @credits ) {
910 #decide which goes first by weight of top (unapplied) line item
912 my @open_lineitems = $self->open_cust_bill_pkg;
915 max( map { $_->part_pkg->pay_weight || 0 }
920 my $max_credit_weight =
921 max( map { $_->part_pkg->credit_weight || 0 }
927 #if both are the same... payments first? it has to be something
928 if ( $max_pay_weight >= $max_credit_weight ) {
934 } elsif ( @payments ) {
936 } elsif ( @credits ) {
939 die "guru meditation #12 and 35";
943 if ( $app eq 'pay' ) {
945 my $payment = shift @payments;
946 $unapp_amount = $payment->unapplied;
947 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
948 $app->pkgnum( $payment->pkgnum )
949 if $conf->exists('pkg-balances') && $payment->pkgnum;
951 } elsif ( $app eq 'credit' ) {
953 my $credit = shift @credits;
954 $unapp_amount = $credit->credited;
955 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
956 $app->pkgnum( $credit->pkgnum )
957 if $conf->exists('pkg-balances') && $credit->pkgnum;
960 die "guru meditation #12 and 35";
964 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
965 warn "owed_pkgnum ". $app->pkgnum;
966 $owed = $self->owed_pkgnum($app->pkgnum);
970 next unless $owed > 0;
972 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
973 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
975 $app->invnum( $self->invnum );
977 my $error = $app->insert(%options);
979 $dbh->rollback if $oldAutoCommit;
980 return "Error inserting ". $app->table. " record: $error";
982 die $error if $error;
986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
991 =item generate_email OPTION => VALUE ...
999 sender address, required
1003 alternate template name, optional
1007 text attachment arrayref, optional
1011 email subject, optional
1015 notice name instead of "Invoice", optional
1019 Returns an argument list to be passed to L<FS::Misc::send_email>.
1025 sub generate_email {
1029 my $conf = $self->conf;
1031 my $me = '[FS::cust_bill::generate_email]';
1034 'from' => $args{'from'},
1035 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1039 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1040 'template' => $args{'template'},
1041 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1042 'no_coupon' => $args{'no_coupon'},
1045 my $cust_main = $self->cust_main;
1047 if (ref($args{'to'}) eq 'ARRAY') {
1048 $return{'to'} = $args{'to'};
1050 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1051 $cust_main->invoicing_list
1055 if ( $conf->exists('invoice_html') ) {
1057 warn "$me creating HTML/text multipart message"
1060 $return{'nobody'} = 1;
1062 my $alternative = build MIME::Entity
1063 'Type' => 'multipart/alternative',
1064 #'Encoding' => '7bit',
1065 'Disposition' => 'inline'
1069 if ( $conf->exists('invoice_email_pdf')
1070 and scalar($conf->config('invoice_email_pdf_note')) ) {
1072 warn "$me using 'invoice_email_pdf_note' in multipart message"
1074 $data = [ map { $_ . "\n" }
1075 $conf->config('invoice_email_pdf_note')
1080 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1082 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1083 $data = $args{'print_text'};
1085 $data = [ $self->print_text(\%opt) ];
1090 $alternative->attach(
1091 'Type' => 'text/plain',
1092 'Encoding' => 'quoted-printable',
1093 #'Encoding' => '7bit',
1095 'Disposition' => 'inline',
1102 if ( $conf->exists('invoice_email_pdf')
1103 and scalar($conf->config('invoice_email_pdf_note')) ) {
1105 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1109 $args{'from'} =~ /\@([\w\.\-]+)/;
1110 my $from = $1 || 'example.com';
1111 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1114 my $agentnum = $cust_main->agentnum;
1115 if ( defined($args{'template'}) && length($args{'template'})
1116 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1119 $logo = 'logo_'. $args{'template'}. '.png';
1123 my $image_data = $conf->config_binary( $logo, $agentnum);
1125 $image = build MIME::Entity
1126 'Type' => 'image/png',
1127 'Encoding' => 'base64',
1128 'Data' => $image_data,
1129 'Filename' => 'logo.png',
1130 'Content-ID' => "<$content_id>",
1133 if ($conf->exists('invoice-barcode')) {
1134 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1135 $barcode = build MIME::Entity
1136 'Type' => 'image/png',
1137 'Encoding' => 'base64',
1138 'Data' => $self->invoice_barcode(0),
1139 'Filename' => 'barcode.png',
1140 'Content-ID' => "<$barcode_content_id>",
1142 $opt{'barcode_cid'} = $barcode_content_id;
1145 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1148 $alternative->attach(
1149 'Type' => 'text/html',
1150 'Encoding' => 'quoted-printable',
1151 'Data' => [ '<html>',
1154 ' '. encode_entities($return{'subject'}),
1157 ' <body bgcolor="#e8e8e8">',
1162 'Disposition' => 'inline',
1163 #'Filename' => 'invoice.pdf',
1167 my @otherparts = ();
1168 if ( $cust_main->email_csv_cdr ) {
1170 push @otherparts, build MIME::Entity
1171 'Type' => 'text/csv',
1172 'Encoding' => '7bit',
1173 'Data' => [ map { "$_\n" }
1174 $self->call_details('prepend_billed_number' => 1)
1176 'Disposition' => 'attachment',
1177 'Filename' => 'usage-'. $self->invnum. '.csv',
1182 if ( $conf->exists('invoice_email_pdf') ) {
1187 # multipart/alternative
1193 my $related = build MIME::Entity 'Type' => 'multipart/related',
1194 'Encoding' => '7bit';
1196 #false laziness w/Misc::send_email
1197 $related->head->replace('Content-type',
1198 $related->mime_type.
1199 '; boundary="'. $related->head->multipart_boundary. '"'.
1200 '; type=multipart/alternative'
1203 $related->add_part($alternative);
1205 $related->add_part($image) if $image;
1207 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1209 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1213 #no other attachment:
1215 # multipart/alternative
1220 $return{'content-type'} = 'multipart/related';
1221 if ($conf->exists('invoice-barcode') && $barcode) {
1222 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1224 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1226 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1227 #$return{'disposition'} = 'inline';
1233 if ( $conf->exists('invoice_email_pdf') ) {
1234 warn "$me creating PDF attachment"
1237 #mime parts arguments a la MIME::Entity->build().
1238 $return{'mimeparts'} = [
1239 { $self->mimebuild_pdf(\%opt) }
1243 if ( $conf->exists('invoice_email_pdf')
1244 and scalar($conf->config('invoice_email_pdf_note')) ) {
1246 warn "$me using 'invoice_email_pdf_note'"
1248 $return{'body'} = [ map { $_ . "\n" }
1249 $conf->config('invoice_email_pdf_note')
1254 warn "$me not using 'invoice_email_pdf_note'"
1256 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1257 $return{'body'} = $args{'print_text'};
1259 $return{'body'} = [ $self->print_text(\%opt) ];
1272 Returns a list suitable for passing to MIME::Entity->build(), representing
1273 this invoice as PDF attachment.
1280 'Type' => 'application/pdf',
1281 'Encoding' => 'base64',
1282 'Data' => [ $self->print_pdf(@_) ],
1283 'Disposition' => 'attachment',
1284 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1288 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1290 Sends this invoice to the destinations configured for this customer: sends
1291 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1293 Options can be passed as a hashref (recommended) or as a list of up to
1294 four values for templatename, agentnum, invoice_from and amount.
1296 I<template>, if specified, is the name of a suffix for alternate invoices.
1298 I<agentnum>, if specified, means that this invoice will only be sent for customers
1299 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1300 single agent) or an arrayref of agentnums.
1302 I<invoice_from>, if specified, overrides the default email invoice From: address.
1304 I<amount>, if specified, only sends the invoice if the total amount owed on this
1305 invoice and all older invoices is greater than the specified amount.
1307 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1311 sub queueable_send {
1314 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1315 or die "invalid invoice number: " . $opt{invnum};
1317 my @args = ( $opt{template}, $opt{agentnum} );
1318 push @args, $opt{invoice_from}
1319 if exists($opt{invoice_from}) && $opt{invoice_from};
1321 my $error = $self->send( @args );
1322 die $error if $error;
1328 my $conf = $self->conf;
1330 my( $template, $invoice_from, $notice_name );
1332 my $balance_over = 0;
1336 $template = $opt->{'template'} || '';
1337 if ( $agentnums = $opt->{'agentnum'} ) {
1338 $agentnums = [ $agentnums ] unless ref($agentnums);
1340 $invoice_from = $opt->{'invoice_from'};
1341 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1342 $notice_name = $opt->{'notice_name'};
1344 $template = scalar(@_) ? shift : '';
1345 if ( scalar(@_) && $_[0] ) {
1346 $agentnums = ref($_[0]) ? shift : [ shift ];
1348 $invoice_from = shift if scalar(@_);
1349 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1352 my $cust_main = $self->cust_main;
1354 return 'N/A' unless ! $agentnums
1355 or grep { $_ == $cust_main->agentnum } @$agentnums;
1358 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1360 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1361 $conf->config('invoice_from', $cust_main->agentnum );
1364 'template' => $template,
1365 'invoice_from' => $invoice_from,
1366 'notice_name' => ( $notice_name || 'Invoice' ),
1369 my @invoicing_list = $cust_main->invoicing_list;
1371 #$self->email_invoice(\%opt)
1373 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1374 && ! $self->invoice_noemail;
1376 #$self->print_invoice(\%opt)
1378 if grep { $_ eq 'POST' } @invoicing_list; #postal
1380 $self->fax_invoice(\%opt)
1381 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1387 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1389 Emails this invoice.
1391 Options can be passed as a hashref (recommended) or as a list of up to
1392 two values for templatename and invoice_from.
1394 I<template>, if specified, is the name of a suffix for alternate invoices.
1396 I<invoice_from>, if specified, overrides the default email invoice From: address.
1398 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1402 sub queueable_email {
1405 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1406 or die "invalid invoice number: " . $opt{invnum};
1408 my %args = ( 'template' => $opt{template} );
1409 $args{$_} = $opt{$_}
1410 foreach grep { exists($opt{$_}) && $opt{$_} }
1411 qw( invoice_from notice_name no_coupon );
1413 my $error = $self->email( \%args );
1414 die $error if $error;
1418 #sub email_invoice {
1421 return if $self->hide;
1422 my $conf = $self->conf;
1424 my( $template, $invoice_from, $notice_name, $no_coupon );
1427 $template = $opt->{'template'} || '';
1428 $invoice_from = $opt->{'invoice_from'};
1429 $notice_name = $opt->{'notice_name'} || 'Invoice';
1430 $no_coupon = $opt->{'no_coupon'} || 0;
1432 $template = scalar(@_) ? shift : '';
1433 $invoice_from = shift if scalar(@_);
1434 $notice_name = 'Invoice';
1438 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1439 $conf->config('invoice_from', $self->cust_main->agentnum );
1441 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1442 $self->cust_main->invoicing_list;
1444 if ( ! @invoicing_list ) { #no recipients
1445 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1446 die 'No recipients for customer #'. $self->custnum;
1448 #default: better to notify this person than silence
1449 @invoicing_list = ($invoice_from);
1453 my $subject = $self->email_subject($template);
1455 my $error = send_email(
1456 $self->generate_email(
1457 'from' => $invoice_from,
1458 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1459 'subject' => $subject,
1460 'template' => $template,
1461 'notice_name' => $notice_name,
1462 'no_coupon' => $no_coupon,
1465 die "can't email invoice: $error\n" if $error;
1466 #die "$error\n" if $error;
1472 my $conf = $self->conf;
1474 #my $template = scalar(@_) ? shift : '';
1477 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1480 my $cust_main = $self->cust_main;
1481 my $name = $cust_main->name;
1482 my $name_short = $cust_main->name_short;
1483 my $invoice_number = $self->invnum;
1484 my $invoice_date = $self->_date_pretty;
1486 eval qq("$subject");
1489 =item lpr_data HASHREF | [ TEMPLATE ]
1491 Returns the postscript or plaintext for this invoice as an arrayref.
1493 Options can be passed as a hashref (recommended) or as a single optional value
1496 I<template>, if specified, is the name of a suffix for alternate invoices.
1498 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1504 my $conf = $self->conf;
1505 my( $template, $notice_name );
1508 $template = $opt->{'template'} || '';
1509 $notice_name = $opt->{'notice_name'} || 'Invoice';
1511 $template = scalar(@_) ? shift : '';
1512 $notice_name = 'Invoice';
1516 'template' => $template,
1517 'notice_name' => $notice_name,
1520 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1521 [ $self->$method( \%opt ) ];
1524 =item print HASHREF | [ TEMPLATE ]
1526 Prints this invoice.
1528 Options can be passed as a hashref (recommended) or as a single optional
1531 I<template>, if specified, is the name of a suffix for alternate invoices.
1533 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1537 #sub print_invoice {
1540 return if $self->hide;
1541 my $conf = $self->conf;
1543 my( $template, $notice_name );
1546 $template = $opt->{'template'} || '';
1547 $notice_name = $opt->{'notice_name'} || 'Invoice';
1549 $template = scalar(@_) ? shift : '';
1550 $notice_name = 'Invoice';
1554 'template' => $template,
1555 'notice_name' => $notice_name,
1558 if($conf->exists('invoice_print_pdf')) {
1559 # Add the invoice to the current batch.
1560 $self->batch_invoice(\%opt);
1564 $self->lpr_data(\%opt),
1565 'agentnum' => $self->cust_main->agentnum,
1570 =item fax_invoice HASHREF | [ TEMPLATE ]
1574 Options can be passed as a hashref (recommended) or as a single optional
1577 I<template>, if specified, is the name of a suffix for alternate invoices.
1579 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1585 return if $self->hide;
1586 my $conf = $self->conf;
1588 my( $template, $notice_name );
1591 $template = $opt->{'template'} || '';
1592 $notice_name = $opt->{'notice_name'} || 'Invoice';
1594 $template = scalar(@_) ? shift : '';
1595 $notice_name = 'Invoice';
1598 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1599 unless $conf->exists('invoice_latex');
1601 my $dialstring = $self->cust_main->getfield('fax');
1605 'template' => $template,
1606 'notice_name' => $notice_name,
1609 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1610 'dialstring' => $dialstring,
1612 die $error if $error;
1616 =item batch_invoice [ HASHREF ]
1618 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1619 isn't an open batch, one will be created.
1624 my ($self, $opt) = @_;
1625 my $bill_batch = $self->get_open_bill_batch;
1626 my $cust_bill_batch = FS::cust_bill_batch->new({
1627 batchnum => $bill_batch->batchnum,
1628 invnum => $self->invnum,
1630 return $cust_bill_batch->insert($opt);
1633 =item get_open_batch
1635 Returns the currently open batch as an FS::bill_batch object, creating a new
1636 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1641 sub get_open_bill_batch {
1643 my $conf = $self->conf;
1644 my $hashref = { status => 'O' };
1645 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1646 ? $self->cust_main->agentnum
1648 my $batch = qsearchs('bill_batch', $hashref);
1649 return $batch if $batch;
1650 $batch = FS::bill_batch->new($hashref);
1651 my $error = $batch->insert;
1652 die $error if $error;
1656 =item ftp_invoice [ TEMPLATENAME ]
1658 Sends this invoice data via FTP.
1660 TEMPLATENAME is unused?
1666 my $conf = $self->conf;
1667 my $template = scalar(@_) ? shift : '';
1670 'protocol' => 'ftp',
1671 'server' => $conf->config('cust_bill-ftpserver'),
1672 'username' => $conf->config('cust_bill-ftpusername'),
1673 'password' => $conf->config('cust_bill-ftppassword'),
1674 'dir' => $conf->config('cust_bill-ftpdir'),
1675 'format' => $conf->config('cust_bill-ftpformat'),
1679 =item spool_invoice [ TEMPLATENAME ]
1681 Spools this invoice data (see L<FS::spool_csv>)
1683 TEMPLATENAME is unused?
1689 my $conf = $self->conf;
1690 my $template = scalar(@_) ? shift : '';
1693 'format' => $conf->config('cust_bill-spoolformat'),
1694 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1698 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1700 Like B<send>, but only sends the invoice if it is the newest open invoice for
1705 sub send_if_newest {
1710 grep { $_->owed > 0 }
1711 qsearch('cust_bill', {
1712 'custnum' => $self->custnum,
1713 #'_date' => { op=>'>', value=>$self->_date },
1714 'invnum' => { op=>'>', value=>$self->invnum },
1721 =item send_csv OPTION => VALUE, ...
1723 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1727 protocol - currently only "ftp"
1733 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1734 and YYMMDDHHMMSS is a timestamp.
1736 See L</print_csv> for a description of the output format.
1741 my($self, %opt) = @_;
1745 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1746 mkdir $spooldir, 0700 unless -d $spooldir;
1748 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1749 my $file = "$spooldir/$tracctnum.csv";
1751 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1753 open(CSV, ">$file") or die "can't open $file: $!";
1761 if ( $opt{protocol} eq 'ftp' ) {
1762 eval "use Net::FTP;";
1764 $net = Net::FTP->new($opt{server}) or die @$;
1766 die "unknown protocol: $opt{protocol}";
1769 $net->login( $opt{username}, $opt{password} )
1770 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1772 $net->binary or die "can't set binary mode";
1774 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1776 $net->put($file) or die "can't put $file: $!";
1786 Spools CSV invoice data.
1792 =item format - 'default' or 'billco'
1794 =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>).
1796 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1798 =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.
1805 my($self, %opt) = @_;
1807 my $cust_main = $self->cust_main;
1809 if ( $opt{'dest'} ) {
1810 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1811 $cust_main->invoicing_list;
1812 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1813 || ! keys %invoicing_list;
1816 if ( $opt{'balanceover'} ) {
1818 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1821 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1822 mkdir $spooldir, 0700 unless -d $spooldir;
1824 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1828 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1829 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1832 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1834 open(CSV, ">>$file") or die "can't open $file: $!";
1835 flock(CSV, LOCK_EX);
1840 if ( lc($opt{'format'}) eq 'billco' ) {
1842 flock(CSV, LOCK_UN);
1847 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1850 open(CSV,">>$file") or die "can't open $file: $!";
1851 flock(CSV, LOCK_EX);
1857 flock(CSV, LOCK_UN);
1864 =item print_csv OPTION => VALUE, ...
1866 Returns CSV data for this invoice.
1870 format - 'default' or 'billco'
1872 Returns a list consisting of two scalars. The first is a single line of CSV
1873 header information for this invoice. The second is one or more lines of CSV
1874 detail information for this invoice.
1876 If I<format> is not specified or "default", the fields of the CSV file are as
1879 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1883 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1885 B<record_type> is C<cust_bill> for the initial header line only. The
1886 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1887 fields are filled in.
1889 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1890 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1893 =item invnum - invoice number
1895 =item custnum - customer number
1897 =item _date - invoice date
1899 =item charged - total invoice amount
1901 =item first - customer first name
1903 =item last - customer first name
1905 =item company - company name
1907 =item address1 - address line 1
1909 =item address2 - address line 1
1919 =item pkg - line item description
1921 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1923 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1925 =item sdate - start date for recurring fee
1927 =item edate - end date for recurring fee
1931 If I<format> is "billco", the fields of the header CSV file are as follows:
1933 +-------------------------------------------------------------------+
1934 | FORMAT HEADER FILE |
1935 |-------------------------------------------------------------------|
1936 | Field | Description | Name | Type | Width |
1937 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1938 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1939 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1940 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1941 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1942 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1943 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1944 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1945 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1946 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1947 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1948 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1949 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1950 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1951 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1952 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1953 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1954 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1955 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1956 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1957 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1958 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1959 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1960 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1961 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1962 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1963 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1964 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1965 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1966 +-------+-------------------------------+------------+------+-------+
1968 If I<format> is "billco", the fields of the detail CSV file are as follows:
1970 FORMAT FOR DETAIL FILE
1972 Field | Description | Name | Type | Width
1973 1 | N/A-Leave Empty | RC | CHAR | 2
1974 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1975 3 | Account Number | TRACCTNUM | CHAR | 15
1976 4 | Invoice Number | TRINVOICE | CHAR | 15
1977 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1978 6 | Transaction Detail | DETAILS | CHAR | 100
1979 7 | Amount | AMT | NUM* | 9
1980 8 | Line Format Control** | LNCTRL | CHAR | 2
1981 9 | Grouping Code | GROUP | CHAR | 2
1982 10 | User Defined | ACCT CODE | CHAR | 15
1987 my($self, %opt) = @_;
1989 eval "use Text::CSV_XS";
1992 my $cust_main = $self->cust_main;
1994 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1996 if ( lc($opt{'format'}) eq 'billco' ) {
1999 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2001 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2003 my( $previous_balance, @unused ) = $self->previous; #previous balance
2005 my $pmt_cr_applied = 0;
2006 $pmt_cr_applied += $_->{'amount'}
2007 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
2009 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2012 '', # 1 | N/A-Leave Empty CHAR 2
2013 '', # 2 | N/A-Leave Empty CHAR 15
2014 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
2015 $self->invnum, # 4 | Transaction Invoice No CHAR 15
2016 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
2017 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
2018 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2019 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2020 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2021 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2022 '', # 10 | Ancillary Billing Information CHAR 30
2023 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2024 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2027 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2030 $duedate, # 14 | Bill Due Date CHAR 10
2032 $previous_balance, # 15 | Previous Balance NUM* 9
2033 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2034 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2035 $totaldue, # 18 | Total Amt Due NUM* 9
2036 $totaldue, # 19 | Total Amt Due NUM* 9
2037 '', # 20 | 30 Day Aging NUM* 9
2038 '', # 21 | 60 Day Aging NUM* 9
2039 '', # 22 | 90 Day Aging NUM* 9
2040 'N', # 23 | Y/N CHAR 1
2041 '', # 24 | Remittance automation CHAR 100
2042 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2043 $self->custnum, # 26 | Customer Reference Number CHAR 15
2044 '0', # 27 | Federal Tax*** NUM* 9
2045 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2046 '0', # 29 | Other Taxes & Fees*** NUM* 9
2049 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2051 my ($previous_balance) = $self->previous;
2052 $previous_balance = sprintf('%.2f', $previous_balance);
2053 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2059 $self->_items_pkg, #_items_nontax? no sections or anything
2064 $cust_main->agentnum,
2065 $cust_main->agent->agent,
2069 $cust_main->company,
2070 $cust_main->address1,
2071 $cust_main->address2,
2077 time2str("%x", $self->_date),
2082 $self->due_date2str("%x"),
2093 time2str("%x", $self->_date),
2094 sprintf("%.2f", $self->charged),
2095 ( map { $cust_main->getfield($_) }
2096 qw( first last company address1 address2 city state zip country ) ),
2098 ) or die "can't create csv";
2101 my $header = $csv->string. "\n";
2104 if ( lc($opt{'format'}) eq 'billco' ) {
2107 foreach my $item ( $self->_items_pkg ) {
2110 '', # 1 | N/A-Leave Empty CHAR 2
2111 '', # 2 | N/A-Leave Empty CHAR 15
2112 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2113 $self->invnum, # 4 | Invoice Number CHAR 15
2114 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2115 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2116 $item->{'amount'}, # 7 | Amount NUM* 9
2117 '', # 8 | Line Format Control** CHAR 2
2118 '', # 9 | Grouping Code CHAR 2
2119 '', # 10 | User Defined CHAR 15
2122 $detail .= $csv->string. "\n";
2126 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2132 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2134 my($pkg, $setup, $recur, $sdate, $edate);
2135 if ( $cust_bill_pkg->pkgnum ) {
2137 ($pkg, $setup, $recur, $sdate, $edate) = (
2138 $cust_bill_pkg->part_pkg->pkg,
2139 ( $cust_bill_pkg->setup != 0
2140 ? sprintf("%.2f", $cust_bill_pkg->setup )
2142 ( $cust_bill_pkg->recur != 0
2143 ? sprintf("%.2f", $cust_bill_pkg->recur )
2145 ( $cust_bill_pkg->sdate
2146 ? time2str("%x", $cust_bill_pkg->sdate)
2148 ($cust_bill_pkg->edate
2149 ?time2str("%x", $cust_bill_pkg->edate)
2153 } else { #pkgnum tax
2154 next unless $cust_bill_pkg->setup != 0;
2155 $pkg = $cust_bill_pkg->desc;
2156 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2157 ( $sdate, $edate ) = ( '', '' );
2163 ( map { '' } (1..11) ),
2164 ($pkg, $setup, $recur, $sdate, $edate)
2165 ) or die "can't create csv";
2167 $detail .= $csv->string. "\n";
2173 ( $header, $detail );
2179 Pays this invoice with a compliemntary payment. If there is an error,
2180 returns the error, otherwise returns false.
2186 my $cust_pay = new FS::cust_pay ( {
2187 'invnum' => $self->invnum,
2188 'paid' => $self->owed,
2191 'payinfo' => $self->cust_main->payinfo,
2199 Attempts to pay this invoice with a credit card payment via a
2200 Business::OnlinePayment realtime gateway. See
2201 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2202 for supported processors.
2208 $self->realtime_bop( 'CC', @_ );
2213 Attempts to pay this invoice with an electronic check (ACH) payment via a
2214 Business::OnlinePayment realtime gateway. See
2215 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2216 for supported processors.
2222 $self->realtime_bop( 'ECHECK', @_ );
2227 Attempts to pay this invoice with phone bill (LEC) payment via a
2228 Business::OnlinePayment realtime gateway. See
2229 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2230 for supported processors.
2236 $self->realtime_bop( 'LEC', @_ );
2240 my( $self, $method ) = (shift,shift);
2241 my $conf = $self->conf;
2244 my $cust_main = $self->cust_main;
2245 my $balance = $cust_main->balance;
2246 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2247 $amount = sprintf("%.2f", $amount);
2248 return "not run (balance $balance)" unless $amount > 0;
2250 my $description = 'Internet Services';
2251 if ( $conf->exists('business-onlinepayment-description') ) {
2252 my $dtempl = $conf->config('business-onlinepayment-description');
2254 my $agent_obj = $cust_main->agent
2255 or die "can't retreive agent for $cust_main (agentnum ".
2256 $cust_main->agentnum. ")";
2257 my $agent = $agent_obj->agent;
2258 my $pkgs = join(', ',
2259 map { $_->part_pkg->pkg }
2260 grep { $_->pkgnum } $self->cust_bill_pkg
2262 $description = eval qq("$dtempl");
2265 $cust_main->realtime_bop($method, $amount,
2266 'description' => $description,
2267 'invnum' => $self->invnum,
2268 #this didn't do what we want, it just calls apply_payments_and_credits
2270 'apply_to_invoice' => 1,
2273 #this changes application behavior: auto payments
2274 #triggered against a specific invoice are now applied
2275 #to that invoice instead of oldest open.
2281 =item batch_card OPTION => VALUE...
2283 Adds a payment for this invoice to the pending credit card batch (see
2284 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2285 runs the payment using a realtime gateway.
2290 my ($self, %options) = @_;
2291 my $cust_main = $self->cust_main;
2293 $options{invnum} = $self->invnum;
2295 $cust_main->batch_card(%options);
2298 sub _agent_template {
2300 $self->cust_main->agent_template;
2303 sub _agent_invoice_from {
2305 $self->cust_main->agent_invoice_from;
2308 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2310 Returns an text invoice, as a list of lines.
2312 Options can be passed as a hashref (recommended) or as a list of time, template
2313 and then any key/value pairs for any other options.
2315 I<time>, if specified, is used to control the printing of overdue messages. The
2316 default is now. It isn't the date of the invoice; that's the `_date' field.
2317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2318 L<Time::Local> and L<Date::Parse> for conversion functions.
2320 I<template>, if specified, is the name of a suffix for alternate invoices.
2322 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2328 my( $today, $template, %opt );
2330 %opt = %{ shift() };
2331 $today = delete($opt{'time'}) || '';
2332 $template = delete($opt{template}) || '';
2334 ( $today, $template, %opt ) = @_;
2337 my %params = ( 'format' => 'template' );
2338 $params{'time'} = $today if $today;
2339 $params{'template'} = $template if $template;
2340 $params{$_} = $opt{$_}
2341 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2343 $self->print_generic( %params );
2346 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2348 Internal method - returns a filename of a filled-in LaTeX template for this
2349 invoice (Note: add ".tex" to get the actual filename), and a filename of
2350 an associated logo (with the .eps extension included).
2352 See print_ps and print_pdf for methods that return PostScript and PDF output.
2354 Options can be passed as a hashref (recommended) or as a list of time, template
2355 and then any key/value pairs for any other options.
2357 I<time>, if specified, is used to control the printing of overdue messages. The
2358 default is now. It isn't the date of the invoice; that's the `_date' field.
2359 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2360 L<Time::Local> and L<Date::Parse> for conversion functions.
2362 I<template>, if specified, is the name of a suffix for alternate invoices.
2364 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2370 my $conf = $self->conf;
2371 my( $today, $template, %opt );
2373 %opt = %{ shift() };
2374 $today = delete($opt{'time'}) || '';
2375 $template = delete($opt{template}) || '';
2377 ( $today, $template, %opt ) = @_;
2380 my %params = ( 'format' => 'latex' );
2381 $params{'time'} = $today if $today;
2382 $params{'template'} = $template if $template;
2383 $params{$_} = $opt{$_}
2384 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2386 $template ||= $self->_agent_template;
2388 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2389 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2393 ) or die "can't open temp file: $!\n";
2395 my $agentnum = $self->cust_main->agentnum;
2397 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2398 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2399 or die "can't write temp file: $!\n";
2401 print $lh $conf->config_binary('logo.eps', $agentnum)
2402 or die "can't write temp file: $!\n";
2405 $params{'logo_file'} = $lh->filename;
2407 if($conf->exists('invoice-barcode')){
2408 my $png_file = $self->invoice_barcode($dir);
2409 my $eps_file = $png_file;
2410 $eps_file =~ s/\.png$/.eps/g;
2411 $png_file =~ /(barcode.*png)/;
2413 $eps_file =~ /(barcode.*eps)/;
2416 my $curr_dir = cwd();
2418 # after painfuly long experimentation, it was determined that sam2p won't
2419 # accept : and other chars in the path, no matter how hard I tried to
2420 # escape them, hence the chdir (and chdir back, just to be safe)
2421 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2422 or die "sam2p failed: $!\n";
2426 $params{'barcode_file'} = $eps_file;
2429 my @filled_in = $self->print_generic( %params );
2431 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2435 ) or die "can't open temp file: $!\n";
2436 binmode($fh, ':utf8'); # language support
2437 print $fh join('', @filled_in );
2440 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2441 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2445 =item invoice_barcode DIR_OR_FALSE
2447 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2448 it is taken as the temp directory where the PNG file will be generated and the
2449 PNG file name is returned. Otherwise, the PNG image itself is returned.
2453 sub invoice_barcode {
2454 my ($self, $dir) = (shift,shift);
2456 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2457 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2458 my $gd = $gdbar->plot(Height => 30);
2461 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2465 ) or die "can't open temp file: $!\n";
2466 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2467 my $png_file = $bh->filename;
2474 =item print_generic OPTION => VALUE ...
2476 Internal method - returns a filled-in template for this invoice as a scalar.
2478 See print_ps and print_pdf for methods that return PostScript and PDF output.
2480 Non optional options include
2481 format - latex, html, template
2483 Optional options include
2485 template - a value used as a suffix for a configuration template
2487 time - a value used to control the printing of overdue messages. The
2488 default is now. It isn't the date of the invoice; that's the `_date' field.
2489 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2490 L<Time::Local> and L<Date::Parse> for conversion functions.
2494 unsquelch_cdr - overrides any per customer cdr squelching when true
2496 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2498 locale - override customer's locale
2502 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2503 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2504 # yes: fixed width/plain text printing will be borked
2506 my( $self, %params ) = @_;
2507 my $conf = $self->conf;
2508 my $today = $params{today} ? $params{today} : time;
2509 warn "$me print_generic called on $self with suffix $params{template}\n"
2512 my $format = $params{format};
2513 die "Unknown format: $format"
2514 unless $format =~ /^(latex|html|template)$/;
2516 my $cust_main = $self->cust_main;
2517 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2518 unless $cust_main->payname
2519 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2521 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2522 'html' => [ '<%=', '%>' ],
2523 'template' => [ '{', '}' ],
2526 warn "$me print_generic creating template\n"
2529 #create the template
2530 my $template = $params{template} ? $params{template} : $self->_agent_template;
2531 my $templatefile = "invoice_$format";
2532 $templatefile .= "_$template"
2533 if length($template) && $conf->exists($templatefile."_$template");
2534 my @invoice_template = map "$_\n", $conf->config($templatefile)
2535 or die "cannot load config data $templatefile";
2538 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2539 #change this to a die when the old code is removed
2540 warn "old-style invoice template $templatefile; ".
2541 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2542 $old_latex = 'true';
2543 @invoice_template = _translate_old_latex_format(@invoice_template);
2546 warn "$me print_generic creating T:T object\n"
2549 my $text_template = new Text::Template(
2551 SOURCE => \@invoice_template,
2552 DELIMITERS => $delimiters{$format},
2555 warn "$me print_generic compiling T:T object\n"
2558 $text_template->compile()
2559 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2562 # additional substitution could possibly cause breakage in existing templates
2563 my %convert_maps = (
2565 'notes' => sub { map "$_", @_ },
2566 'footer' => sub { map "$_", @_ },
2567 'smallfooter' => sub { map "$_", @_ },
2568 'returnaddress' => sub { map "$_", @_ },
2569 'coupon' => sub { map "$_", @_ },
2570 'summary' => sub { map "$_", @_ },
2576 s/%%(.*)$/<!-- $1 -->/g;
2577 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2578 s/\\begin\{enumerate\}/<ol>/g;
2580 s/\\end\{enumerate\}/<\/ol>/g;
2581 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2590 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2592 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2597 s/\\\\\*?\s*$/<BR>/;
2598 s/\\hyphenation\{[\w\s\-]+}//;
2603 'coupon' => sub { "" },
2604 'summary' => sub { "" },
2611 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2612 s/\\begin\{enumerate\}//g;
2614 s/\\end\{enumerate\}//g;
2615 s/\\textbf\{(.*)\}/$1/g;
2622 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2624 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2629 s/\\\\\*?\s*$/\n/; # dubious
2630 s/\\hyphenation\{[\w\s\-]+}//;
2634 'coupon' => sub { "" },
2635 'summary' => sub { "" },
2640 # hashes for differing output formats
2641 my %nbsps = ( 'latex' => '~',
2642 'html' => '', # '&nbps;' would be nice
2643 'template' => '', # not used
2645 my $nbsp = $nbsps{$format};
2647 my %escape_functions = ( 'latex' => \&_latex_escape,
2648 'html' => \&_html_escape_nbsp,#\&encode_entities,
2649 'template' => sub { shift },
2651 my $escape_function = $escape_functions{$format};
2652 my $escape_function_nonbsp = ($format eq 'html')
2653 ? \&_html_escape : $escape_function;
2655 my %date_formats = ( 'latex' => $date_format_long,
2656 'html' => $date_format_long,
2659 $date_formats{'html'} =~ s/ / /g;
2661 my $date_format = $date_formats{$format};
2663 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2665 'html' => sub { return '<b>'. shift(). '</b>'
2667 'template' => sub { shift },
2669 my $embolden_function = $embolden_functions{$format};
2671 my %newline_tokens = ( 'latex' => '\\\\',
2675 my $newline_token = $newline_tokens{$format};
2677 warn "$me generating template variables\n"
2680 # generate template variables
2683 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2687 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2693 $returnaddress = join("\n",
2694 $conf->config_orbase("invoice_${format}returnaddress", $template)
2697 } elsif ( grep /\S/,
2698 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2700 my $convert_map = $convert_maps{$format}{'returnaddress'};
2703 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2708 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2710 my $convert_map = $convert_maps{$format}{'returnaddress'};
2711 $returnaddress = join( "\n", &$convert_map(
2712 map { s/( {2,})/'~' x length($1)/eg;
2716 ( $conf->config('company_name', $self->cust_main->agentnum),
2717 $conf->config('company_address', $self->cust_main->agentnum),
2724 my $warning = "Couldn't find a return address; ".
2725 "do you need to set the company_address configuration value?";
2727 $returnaddress = $nbsp;
2728 #$returnaddress = $warning;
2732 warn "$me generating invoice data\n"
2735 my $agentnum = $self->cust_main->agentnum;
2737 my %invoice_data = (
2740 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2741 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2742 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2743 'returnaddress' => $returnaddress,
2744 'agent' => &$escape_function($cust_main->agent->agent),
2747 'invnum' => $self->invnum,
2748 '_date' => $self->_date,
2749 'date' => time2str($date_format, $self->_date),
2750 'today' => time2str($date_format_long, $today),
2751 'terms' => $self->terms,
2752 'template' => $template, #params{'template'},
2753 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2754 'current_charges' => sprintf("%.2f", $self->charged),
2755 'duedate' => $self->due_date2str($rdate_format), #date_format?
2758 'custnum' => $cust_main->display_custnum,
2759 'agent_custid' => &$escape_function($cust_main->agent_custid),
2760 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2761 payname company address1 address2 city state zip fax
2765 'ship_enable' => $conf->exists('invoice-ship_address'),
2766 'unitprices' => $conf->exists('invoice-unitprice'),
2767 'smallernotes' => $conf->exists('invoice-smallernotes'),
2768 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2769 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2771 #layout info -- would be fancy to calc some of this and bury the template
2773 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2774 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2775 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2776 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2777 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2778 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2779 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2780 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2781 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2782 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2784 # better hang on to conf_dir for a while (for old templates)
2785 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2787 #these are only used when doing paged plaintext
2794 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2795 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2796 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2797 # eval to avoid death for unimplemented languages
2798 my $dh = eval { Date::Language->new($info{'name'}) } ||
2799 Date::Language->new(); # fall back to English
2800 # prototype here to silence warnings
2801 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2802 # eventually use this date handle everywhere in here, too
2804 my $min_sdate = 999999999999;
2806 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2807 next unless $cust_bill_pkg->pkgnum > 0;
2808 $min_sdate = $cust_bill_pkg->sdate
2809 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2810 $max_edate = $cust_bill_pkg->edate
2811 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2814 $invoice_data{'bill_period'} = '';
2815 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2816 . " to " . time2str('%e %h', $max_edate)
2817 if ($max_edate != 0 && $min_sdate != 999999999999);
2819 $invoice_data{finance_section} = '';
2820 if ( $conf->config('finance_pkgclass') ) {
2822 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2823 $invoice_data{finance_section} = $pkg_class->categoryname;
2825 $invoice_data{finance_amount} = '0.00';
2826 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2828 my $countrydefault = $conf->config('countrydefault') || 'US';
2829 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2830 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2831 my $method = $prefix.$_;
2832 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2834 $invoice_data{'ship_country'} = ''
2835 if ( $invoice_data{'ship_country'} eq $countrydefault );
2837 $invoice_data{'cid'} = $params{'cid'}
2840 if ( $cust_main->country eq $countrydefault ) {
2841 $invoice_data{'country'} = '';
2843 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2847 $invoice_data{'address'} = \@address;
2849 $cust_main->payname.
2850 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2851 ? " (P.O. #". $cust_main->payinfo. ")"
2855 push @address, $cust_main->company
2856 if $cust_main->company;
2857 push @address, $cust_main->address1;
2858 push @address, $cust_main->address2
2859 if $cust_main->address2;
2861 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2862 push @address, $invoice_data{'country'}
2863 if $invoice_data{'country'};
2865 while (scalar(@address) < 5);
2867 $invoice_data{'logo_file'} = $params{'logo_file'}
2868 if $params{'logo_file'};
2869 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2870 if $params{'barcode_file'};
2871 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2872 if $params{'barcode_img'};
2873 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2874 if $params{'barcode_cid'};
2876 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2877 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2878 #my $balance_due = $self->owed + $pr_total - $cr_total;
2879 my $balance_due = $self->owed + $pr_total;
2881 # the customer's current balance as shown on the invoice before this one
2882 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2884 # the change in balance from that invoice to this one
2885 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2887 # the sum of amount owed on all previous invoices
2888 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2890 # the sum of amount owed on all invoices
2891 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2893 # info from customer's last invoice before this one, for some
2895 $invoice_data{'last_bill'} = {};
2896 if ( $self->previous_bill ) {
2897 $invoice_data{'last_bill'} = {
2898 '_date' => $self->previous_bill->_date, #unformatted
2899 # all we need for now
2903 my $summarypage = '';
2904 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2907 $invoice_data{'summarypage'} = $summarypage;
2909 warn "$me substituting variables in notes, footer, smallfooter\n"
2912 my @include = (qw( notes footer smallfooter ));
2913 push @include, 'coupon' unless $params{'no_coupon'};
2914 foreach my $include (@include) {
2916 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2919 if ( $conf->exists($inc_file, $agentnum)
2920 && length( $conf->config($inc_file, $agentnum) ) ) {
2922 @inc_src = $conf->config($inc_file, $agentnum);
2926 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2928 my $convert_map = $convert_maps{$format}{$include};
2930 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2931 s/--\@\]/$delimiters{$format}[1]/g;
2934 &$convert_map( $conf->config($inc_file, $agentnum) );
2938 my $inc_tt = new Text::Template (
2940 SOURCE => [ map "$_\n", @inc_src ],
2941 DELIMITERS => $delimiters{$format},
2942 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2944 unless ( $inc_tt->compile() ) {
2945 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2946 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2950 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2952 $invoice_data{$include} =~ s/\n+$//
2953 if ($format eq 'latex');
2956 # let invoices use either of these as needed
2957 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2958 ? $cust_main->payinfo : '';
2959 $invoice_data{'po_line'} =
2960 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2961 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2964 my %money_chars = ( 'latex' => '',
2965 'html' => $conf->config('money_char') || '$',
2968 my $money_char = $money_chars{$format};
2970 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2971 'html' => $conf->config('money_char') || '$',
2974 my $other_money_char = $other_money_chars{$format};
2975 $invoice_data{'dollar'} = $other_money_char;
2977 my @detail_items = ();
2978 my @total_items = ();
2982 $invoice_data{'detail_items'} = \@detail_items;
2983 $invoice_data{'total_items'} = \@total_items;
2984 $invoice_data{'buf'} = \@buf;
2985 $invoice_data{'sections'} = \@sections;
2987 warn "$me generating sections\n"
2990 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2991 'subtotal' => $other_money_char.
2992 sprintf('%.2f', $pr_total),
2993 'summarized' => '', #why? $summarypage ? 'Y' : '',
2995 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2996 join(' / ', map { $cust_main->balance_date_range(@$_) }
2997 $self->_prior_month30s
2999 if $conf->exists('invoice_include_aging');
3002 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
3003 'subtotal' => $taxtotal, # adjusted below
3006 my $tax_weight = _pkg_category($tax_section->{description})
3007 ? _pkg_category($tax_section->{description})->weight
3009 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3010 $tax_section->{'sort_weight'} = $tax_weight;
3013 my $adjusttotal = 0;
3014 my $adjust_section = {
3015 'description' => $self->mt('Credits, Payments, and Adjustments'),
3016 'adjust_section' => 1,
3017 'subtotal' => 0, # adjusted below
3019 my $adjust_weight = _pkg_category($adjust_section->{description})
3020 ? _pkg_category($adjust_section->{description})->weight
3022 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3023 $adjust_section->{'sort_weight'} = $adjust_weight;
3025 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3026 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3027 $invoice_data{'multisection'} = $multisection;
3028 my $late_sections = [];
3029 my $extra_sections = [];
3030 my $extra_lines = ();
3032 my $default_section = { 'description' => '',
3037 if ( $multisection ) {
3038 ($extra_sections, $extra_lines) =
3039 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3040 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3042 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3044 push @detail_items, @$extra_lines if $extra_lines;
3046 $self->_items_sections( $late_sections, # this could stand a refactor
3048 $escape_function_nonbsp,
3052 if ($conf->exists('svc_phone_sections')) {
3053 my ($phone_sections, $phone_lines) =
3054 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3055 push @{$late_sections}, @$phone_sections;
3056 push @detail_items, @$phone_lines;
3058 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3059 my ($accountcode_section, $accountcode_lines) =
3060 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3061 if ( scalar(@$accountcode_lines) ) {
3062 push @{$late_sections}, $accountcode_section;
3063 push @detail_items, @$accountcode_lines;
3066 } else {# not multisection
3067 # make a default section
3068 push @sections, $default_section;
3069 # and calculate the finance charge total, since it won't get done otherwise.
3070 # XXX possibly other totals?
3071 # XXX possibly finance_pkgclass should not be used in this manner?
3072 if ( $conf->exists('finance_pkgclass') ) {
3073 my @finance_charges;
3074 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3075 if ( grep { $_->section eq $invoice_data{finance_section} }
3076 $cust_bill_pkg->cust_bill_pkg_display ) {
3077 # I think these are always setup fees, but just to be sure...
3078 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3081 $invoice_data{finance_amount} =
3082 sprintf('%.2f', sum( @finance_charges ) || 0);
3086 # previous invoice balances in the Previous Charges section if there
3087 # is one, otherwise in the main detail section
3088 if ( $self->can('_items_previous') &&
3089 $self->enable_previous &&
3090 ! $conf->exists('previous_balance-summary_only') ) {
3092 warn "$me adding previous balances\n"
3095 foreach my $line_item ( $self->_items_previous ) {
3098 ext_description => [],
3100 $detail->{'ref'} = $line_item->{'pkgnum'};
3101 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3102 $detail->{'quantity'} = 1;
3103 $detail->{'section'} = $multisection ? $previous_section
3105 $detail->{'description'} = &$escape_function($line_item->{'description'});
3106 if ( exists $line_item->{'ext_description'} ) {
3107 @{$detail->{'ext_description'}} = map {
3108 &$escape_function($_);
3109 } @{$line_item->{'ext_description'}};
3111 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3112 $line_item->{'amount'};
3113 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3115 push @detail_items, $detail;
3116 push @buf, [ $detail->{'description'},
3117 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3123 if ( @pr_cust_bill && $self->enable_previous ) {
3124 push @buf, ['','-----------'];
3125 push @buf, [ $self->mt('Total Previous Balance'),
3126 $money_char. sprintf("%10.2f", $pr_total) ];
3130 if ( $conf->exists('svc_phone-did-summary') ) {
3131 warn "$me adding DID summary\n"
3134 my ($didsummary,$minutes) = $self->_did_summary;
3135 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3137 { 'description' => $didsummary_desc,
3138 'ext_description' => [ $didsummary, $minutes ],
3142 foreach my $section (@sections, @$late_sections) {
3144 warn "$me adding section \n". Dumper($section)
3147 # begin some normalization
3148 $section->{'subtotal'} = $section->{'amount'}
3150 && !exists($section->{subtotal})
3151 && exists($section->{amount});
3153 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3154 if ( $invoice_data{finance_section} &&
3155 $section->{'description'} eq $invoice_data{finance_section} );
3157 $section->{'subtotal'} = $other_money_char.
3158 sprintf('%.2f', $section->{'subtotal'})
3161 # continue some normalization
3162 $section->{'amount'} = $section->{'subtotal'}
3166 if ( $section->{'description'} ) {
3167 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3172 warn "$me setting options\n"
3175 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3177 $options{'section'} = $section if $multisection;
3178 $options{'format'} = $format;
3179 $options{'escape_function'} = $escape_function;
3180 $options{'no_usage'} = 1 unless $unsquelched;
3181 $options{'unsquelched'} = $unsquelched;
3182 $options{'summary_page'} = $summarypage;
3183 $options{'skip_usage'} =
3184 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3185 $options{'multilocation'} = $multilocation;
3186 $options{'multisection'} = $multisection;
3188 warn "$me searching for line items\n"
3191 foreach my $line_item ( $self->_items_pkg(%options) ) {
3193 warn "$me adding line item $line_item\n"
3197 ext_description => [],
3199 $detail->{'ref'} = $line_item->{'pkgnum'};
3200 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3201 $detail->{'quantity'} = $line_item->{'quantity'};
3202 $detail->{'section'} = $section;
3203 $detail->{'description'} = &$escape_function($line_item->{'description'});
3204 if ( exists $line_item->{'ext_description'} ) {
3205 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3207 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3208 $line_item->{'amount'};
3209 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3210 $line_item->{'unit_amount'};
3211 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3213 $detail->{'sdate'} = $line_item->{'sdate'};
3214 $detail->{'edate'} = $line_item->{'edate'};
3215 $detail->{'seconds'} = $line_item->{'seconds'};
3216 $detail->{'svc_label'} = $line_item->{'svc_label'};
3218 push @detail_items, $detail;
3219 push @buf, ( [ $detail->{'description'},
3220 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3222 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3226 if ( $section->{'description'} ) {
3227 push @buf, ( ['','-----------'],
3228 [ $section->{'description'}. ' sub-total',
3229 $section->{'subtotal'} # already formatted this
3238 $invoice_data{current_less_finance} =
3239 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3241 # create a major section for previous balance if we have major sections,
3242 # or if previous_section is in summary form
3243 if ( ( $multisection && $self->enable_previous )
3244 || $conf->exists('previous_balance-summary_only') )
3246 unshift @sections, $previous_section if $pr_total;
3249 warn "$me adding taxes\n"
3252 foreach my $tax ( $self->_items_tax ) {
3254 $taxtotal += $tax->{'amount'};
3256 my $description = &$escape_function( $tax->{'description'} );
3257 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3259 if ( $multisection ) {
3261 my $money = $old_latex ? '' : $money_char;
3262 push @detail_items, {
3263 ext_description => [],
3266 description => $description,
3267 amount => $money. $amount,
3269 section => $tax_section,
3274 push @total_items, {
3275 'total_item' => $description,
3276 'total_amount' => $other_money_char. $amount,
3281 push @buf,[ $description,
3282 $money_char. $amount,
3289 $total->{'total_item'} = $self->mt('Sub-total');
3290 $total->{'total_amount'} =
3291 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3293 if ( $multisection ) {
3294 $tax_section->{'subtotal'} = $other_money_char.
3295 sprintf('%.2f', $taxtotal);
3296 $tax_section->{'pretotal'} = 'New charges sub-total '.
3297 $total->{'total_amount'};
3298 push @sections, $tax_section if $taxtotal;
3300 unshift @total_items, $total;
3303 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3305 push @buf,['','-----------'];
3306 push @buf,[$self->mt(
3307 (!$self->enable_previous)
3309 : 'Total New Charges'
3311 $money_char. sprintf("%10.2f",$self->charged) ];
3314 # calculate total, possibly including total owed on previous
3319 $item = $conf->config('previous_balance-exclude_from_total')
3320 || 'Total New Charges'
3321 if $conf->exists('previous_balance-exclude_from_total');
3322 my $amount = $self->charged;
3323 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3324 $amount += $pr_total;
3327 $total->{'total_item'} = &$embolden_function($self->mt($item));
3328 $total->{'total_amount'} =
3329 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3330 if ( $multisection ) {
3331 if ( $adjust_section->{'sort_weight'} ) {
3332 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3333 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3335 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3336 $other_money_char. sprintf('%.2f', $self->charged );
3339 push @total_items, $total;
3341 push @buf,['','-----------'];
3344 sprintf( '%10.2f', $amount )
3349 # if we're showing previous invoices, also show previous
3350 # credits and payments
3351 if ( $self->enable_previous
3352 and $self->can('_items_credits')
3353 and $self->can('_items_payments') )
3355 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3358 my $credittotal = 0;
3359 foreach my $credit (
3360 $self->_items_credits( 'template' => $template, 'trim_len' => 60)
3364 $total->{'total_item'} = &$escape_function($credit->{'description'});
3365 $credittotal += $credit->{'amount'};
3366 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3367 $adjusttotal += $credit->{'amount'};
3368 if ( $multisection ) {
3369 my $money = $old_latex ? '' : $money_char;
3370 push @detail_items, {
3371 ext_description => [],
3374 description => &$escape_function($credit->{'description'}),
3375 amount => $money. $credit->{'amount'},
3377 section => $adjust_section,
3380 push @total_items, $total;
3384 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3387 foreach my $credit (
3388 $self->_items_credits( 'template' => $template, 'trim_len' => 32)
3390 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3394 my $paymenttotal = 0;
3395 foreach my $payment (
3396 $self->_items_payments( 'template' => $template )
3399 $total->{'total_item'} = &$escape_function($payment->{'description'});
3400 $paymenttotal += $payment->{'amount'};
3401 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3402 $adjusttotal += $payment->{'amount'};
3403 if ( $multisection ) {
3404 my $money = $old_latex ? '' : $money_char;
3405 push @detail_items, {
3406 ext_description => [],
3409 description => &$escape_function($payment->{'description'}),
3410 amount => $money. $payment->{'amount'},
3412 section => $adjust_section,
3415 push @total_items, $total;
3417 push @buf, [ $payment->{'description'},
3418 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3421 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3423 if ( $multisection ) {
3424 $adjust_section->{'subtotal'} = $other_money_char.
3425 sprintf('%.2f', $adjusttotal);
3426 push @sections, $adjust_section
3427 unless $adjust_section->{sort_weight};
3430 # create Balance Due message
3433 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3434 $total->{'total_amount'} =
3435 &$embolden_function(
3436 $other_money_char. sprintf('%.2f', #why? $summarypage
3437 # ? $self->charged +
3438 # $self->billing_balance
3440 $self->owed + $pr_total
3443 if ( $multisection && !$adjust_section->{sort_weight} ) {
3444 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3445 $total->{'total_amount'};
3447 push @total_items, $total;
3449 push @buf,['','-----------'];
3450 push @buf,[$self->balance_due_msg, $money_char.
3451 sprintf("%10.2f", $balance_due ) ];
3454 if ( $conf->exists('previous_balance-show_credit')
3455 and $cust_main->balance < 0 ) {
3456 my $credit_total = {
3457 'total_item' => &$embolden_function($self->credit_balance_msg),
3458 'total_amount' => &$embolden_function(
3459 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3462 if ( $multisection ) {
3463 $adjust_section->{'posttotal'} .= $newline_token .
3464 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3467 push @total_items, $credit_total;
3469 push @buf,['','-----------'];
3470 push @buf,[$self->credit_balance_msg, $money_char.
3471 sprintf("%10.2f", -$cust_main->balance ) ];
3475 if ( $multisection ) {
3476 if ($conf->exists('svc_phone_sections')) {
3478 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3479 $total->{'total_amount'} =
3480 &$embolden_function(
3481 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3483 my $last_section = pop @sections;
3484 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3485 $total->{'total_amount'};
3486 push @sections, $last_section;
3488 push @sections, @$late_sections
3492 # make a discounts-available section, even without multisection
3493 if ( $conf->exists('discount-show_available')
3494 and my @discounts_avail = $self->_items_discounts_avail ) {
3495 my $discount_section = {
3496 'description' => $self->mt('Discounts Available'),
3501 push @sections, $discount_section;
3502 push @detail_items, map { +{
3503 'ref' => '', #should this be something else?
3504 'section' => $discount_section,
3505 'description' => &$escape_function( $_->{description} ),
3506 'amount' => $money_char . &$escape_function( $_->{amount} ),
3507 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3508 } } @discounts_avail;
3511 # debugging hook: call this with 'diag' => 1 to just get a hash of
3512 # the invoice variables
3513 return \%invoice_data if ( $params{'diag'} );
3515 # All sections and items are built; now fill in templates.
3516 my @includelist = ();
3517 push @includelist, 'summary' if $summarypage;
3518 foreach my $include ( @includelist ) {
3520 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3523 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3525 @inc_src = $conf->config($inc_file, $agentnum);
3529 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3531 my $convert_map = $convert_maps{$format}{$include};
3533 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3534 s/--\@\]/$delimiters{$format}[1]/g;
3537 &$convert_map( $conf->config($inc_file, $agentnum) );
3541 my $inc_tt = new Text::Template (
3543 SOURCE => [ map "$_\n", @inc_src ],
3544 DELIMITERS => $delimiters{$format},
3545 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3547 unless ( $inc_tt->compile() ) {
3548 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3549 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3553 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3555 $invoice_data{$include} =~ s/\n+$//
3556 if ($format eq 'latex');
3561 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3562 /invoice_lines\((\d*)\)/;
3563 $invoice_lines += $1 || scalar(@buf);
3566 die "no invoice_lines() functions in template?"
3567 if ( $format eq 'template' && !$wasfunc );
3569 if ($format eq 'template') {
3571 if ( $invoice_lines ) {
3572 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3573 $invoice_data{'total_pages'}++
3574 if scalar(@buf) % $invoice_lines;
3577 #setup subroutine for the template
3578 $invoice_data{invoice_lines} = sub {
3579 my $lines = shift || scalar(@buf);
3591 push @collect, split("\n",
3592 $text_template->fill_in( HASH => \%invoice_data )
3594 $invoice_data{'page'}++;
3596 map "$_\n", @collect;
3598 # this is where we actually create the invoice
3599 warn "filling in template for invoice ". $self->invnum. "\n"
3601 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3604 $text_template->fill_in(HASH => \%invoice_data);
3608 # helper routine for generating date ranges
3609 sub _prior_month30s {
3612 [ 1, 2592000 ], # 0-30 days ago
3613 [ 2592000, 5184000 ], # 30-60 days ago
3614 [ 5184000, 7776000 ], # 60-90 days ago
3615 [ 7776000, 0 ], # 90+ days ago
3618 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3619 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3624 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3626 Returns an postscript invoice, as a scalar.
3628 Options can be passed as a hashref (recommended) or as a list of time, template
3629 and then any key/value pairs for any other options.
3631 I<time> an optional value used to control the printing of overdue messages. The
3632 default is now. It isn't the date of the invoice; that's the `_date' field.
3633 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3634 L<Time::Local> and L<Date::Parse> for conversion functions.
3636 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3643 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3644 my $ps = generate_ps($file);
3646 unlink($barcodefile) if $barcodefile;
3651 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3653 Returns an PDF invoice, as a scalar.
3655 Options can be passed as a hashref (recommended) or as a list of time, template
3656 and then any key/value pairs for any other options.
3658 I<time> an optional value used to control the printing of overdue messages. The
3659 default is now. It isn't the date of the invoice; that's the `_date' field.
3660 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3661 L<Time::Local> and L<Date::Parse> for conversion functions.
3663 I<template>, if specified, is the name of a suffix for alternate invoices.
3665 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3672 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3673 my $pdf = generate_pdf($file);
3675 unlink($barcodefile) if $barcodefile;
3680 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3682 Returns an HTML invoice, as a scalar.
3684 I<time> an optional value used to control the printing of overdue messages. The
3685 default is now. It isn't the date of the invoice; that's the `_date' field.
3686 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3687 L<Time::Local> and L<Date::Parse> for conversion functions.
3689 I<template>, if specified, is the name of a suffix for alternate invoices.
3691 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3693 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3694 when emailing the invoice as part of a multipart/related MIME email.
3702 %params = %{ shift() };
3704 $params{'time'} = shift;
3705 $params{'template'} = shift;
3706 $params{'cid'} = shift;
3709 $params{'format'} = 'html';
3711 $self->print_generic( %params );
3714 # quick subroutine for print_latex
3716 # There are ten characters that LaTeX treats as special characters, which
3717 # means that they do not simply typeset themselves:
3718 # # $ % & ~ _ ^ \ { }
3720 # TeX ignores blanks following an escaped character; if you want a blank (as
3721 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3725 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3726 $value =~ s/([<>])/\$$1\$/g;
3732 encode_entities($value);
3736 sub _html_escape_nbsp {
3737 my $value = _html_escape(shift);
3738 $value =~ s/ +/ /g;
3742 #utility methods for print_*
3744 sub _translate_old_latex_format {
3745 warn "_translate_old_latex_format called\n"
3752 if ( $line =~ /^%%Detail\s*$/ ) {
3754 push @template, q![@--!,
3755 q! foreach my $_tr_line (@detail_items) {!,
3756 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3757 q! $_tr_line->{'description'} .= !,
3758 q! "\\tabularnewline\n~~".!,
3759 q! join( "\\tabularnewline\n~~",!,
3760 q! @{$_tr_line->{'ext_description'}}!,
3764 while ( ( my $line_item_line = shift )
3765 !~ /^%%EndDetail\s*$/ ) {
3766 $line_item_line =~ s/'/\\'/g; # nice LTS
3767 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3768 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3769 push @template, " \$OUT .= '$line_item_line';";
3772 push @template, '}',
3775 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3777 push @template, '[@--',
3778 ' foreach my $_tr_line (@total_items) {';
3780 while ( ( my $total_item_line = shift )
3781 !~ /^%%EndTotalDetails\s*$/ ) {
3782 $total_item_line =~ s/'/\\'/g; # nice LTS
3783 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3784 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3785 push @template, " \$OUT .= '$total_item_line';";
3788 push @template, '}',
3792 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3793 push @template, $line;
3799 warn "$_\n" foreach @template;
3807 my $conf = $self->conf;
3809 #check for an invoice-specific override
3810 return $self->invoice_terms if $self->invoice_terms;
3812 #check for a customer- specific override
3813 my $cust_main = $self->cust_main;
3814 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3816 #use configured default
3817 $conf->config('invoice_default_terms') || '';
3823 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3824 $duedate = $self->_date() + ( $1 * 86400 );
3831 $self->due_date ? time2str(shift, $self->due_date) : '';
3834 sub balance_due_msg {
3836 my $msg = $self->mt('Balance Due');
3837 return $msg unless $self->terms;
3838 if ( $self->due_date ) {
3839 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3840 $self->due_date2str($date_format);
3841 } elsif ( $self->terms ) {
3842 $msg .= ' - '. $self->terms;
3847 sub balance_due_date {
3849 my $conf = $self->conf;
3851 if ( $conf->exists('invoice_default_terms')
3852 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3853 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3858 sub credit_balance_msg {
3860 $self->mt('Credit Balance Remaining')
3863 =item invnum_date_pretty
3865 Returns a string with the invoice number and date, for example:
3866 "Invoice #54 (3/20/2008)"
3870 sub invnum_date_pretty {
3872 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3877 Returns a string with the date, for example: "3/20/2008"
3883 time2str($date_format, $self->_date);
3886 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3888 Generate section information for all items appearing on this invoice.
3889 This will only be called for multi-section invoices.
3891 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3892 related display records (L<FS::cust_bill_pkg_display>) and organize
3893 them into two groups ("early" and "late" according to whether they come
3894 before or after the total), then into sections. A subtotal is calculated
3897 Section descriptions are returned in sort weight order. Each consists
3898 of a hash containing:
3900 description: the package category name, escaped
3901 subtotal: the total charges in that section
3902 tax_section: a flag indicating that the section contains only tax charges
3903 summarized: same as tax_section, for some reason
3904 sort_weight: the package category's sort weight
3906 If 'condense' is set on the display record, it also contains everything
3907 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3908 coderefs to generate parts of the invoice. This is not advised.
3912 LATE: an arrayref to push the "late" section hashes onto. The "early"
3913 group is simply returned from the method.
3915 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3916 Turning this on has the following effects:
3917 - Ignores display items with the 'summary' flag.
3918 - Combines all items into the "early" group.
3919 - Creates sections for all non-disabled package categories, even if they
3920 have no charges on this invoice, as well as a section with no name.
3922 ESCAPE: an escape function to use for section titles.
3924 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3925 sorted list. If there are any of these, section subtotals exclude
3928 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3929 passed through to C<_condense_section()>.
3933 use vars qw(%pkg_category_cache);
3934 sub _items_sections {
3937 my $summarypage = shift;
3939 my $extra_sections = shift;
3943 my %late_subtotal = ();
3946 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3949 my $usage = $cust_bill_pkg->usage;
3951 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3952 next if ( $display->summary && $summarypage );
3954 my $section = $display->section;
3955 my $type = $display->type;
3957 $not_tax{$section} = 1
3958 unless $cust_bill_pkg->pkgnum == 0;
3960 if ( $display->post_total && !$summarypage ) {
3961 if (! $type || $type eq 'S') {
3962 $late_subtotal{$section} += $cust_bill_pkg->setup
3963 if $cust_bill_pkg->setup != 0
3964 || $cust_bill_pkg->setup_show_zero;
3968 $late_subtotal{$section} += $cust_bill_pkg->recur
3969 if $cust_bill_pkg->recur != 0
3970 || $cust_bill_pkg->recur_show_zero;
3973 if ($type && $type eq 'R') {
3974 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3975 if $cust_bill_pkg->recur != 0
3976 || $cust_bill_pkg->recur_show_zero;
3979 if ($type && $type eq 'U') {
3980 $late_subtotal{$section} += $usage
3981 unless scalar(@$extra_sections);
3986 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3988 if (! $type || $type eq 'S') {
3989 $subtotal{$section} += $cust_bill_pkg->setup
3990 if $cust_bill_pkg->setup != 0
3991 || $cust_bill_pkg->setup_show_zero;
3995 $subtotal{$section} += $cust_bill_pkg->recur
3996 if $cust_bill_pkg->recur != 0
3997 || $cust_bill_pkg->recur_show_zero;
4000 if ($type && $type eq 'R') {
4001 $subtotal{$section} += $cust_bill_pkg->recur - $usage
4002 if $cust_bill_pkg->recur != 0
4003 || $cust_bill_pkg->recur_show_zero;
4006 if ($type && $type eq 'U') {
4007 $subtotal{$section} += $usage
4008 unless scalar(@$extra_sections);
4017 %pkg_category_cache = ();
4019 push @$late, map { { 'description' => &{$escape}($_),
4020 'subtotal' => $late_subtotal{$_},
4022 'sort_weight' => ( _pkg_category($_)
4023 ? _pkg_category($_)->weight
4026 ((_pkg_category($_) && _pkg_category($_)->condense)
4027 ? $self->_condense_section($format)
4031 sort _sectionsort keys %late_subtotal;
4034 if ( $summarypage ) {
4035 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4036 map { $_->categoryname } qsearch('pkg_category', {});
4037 push @sections, '' if exists($subtotal{''});
4039 @sections = keys %subtotal;
4042 my @early = map { { 'description' => &{$escape}($_),
4043 'subtotal' => $subtotal{$_},
4044 'summarized' => $not_tax{$_} ? '' : 'Y',
4045 'tax_section' => $not_tax{$_} ? '' : 'Y',
4046 'sort_weight' => ( _pkg_category($_)
4047 ? _pkg_category($_)->weight
4050 ((_pkg_category($_) && _pkg_category($_)->condense)
4051 ? $self->_condense_section($format)
4056 push @early, @$extra_sections if $extra_sections;
4058 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4062 #helper subs for above
4065 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4069 my $categoryname = shift;
4070 $pkg_category_cache{$categoryname} ||=
4071 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4074 my %condensed_format = (
4075 'label' => [ qw( Description Qty Amount ) ],
4077 sub { shift->{description} },
4078 sub { shift->{quantity} },
4079 sub { my($href, %opt) = @_;
4080 ($opt{dollar} || ''). $href->{amount};
4083 'align' => [ qw( l r r ) ],
4084 'span' => [ qw( 5 1 1 ) ], # unitprices?
4085 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4088 sub _condense_section {
4089 my ( $self, $format ) = ( shift, shift );
4091 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4092 qw( description_generator
4095 total_line_generator
4100 sub _condensed_generator_defaults {
4101 my ( $self, $format ) = ( shift, shift );
4102 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4111 sub _condensed_header_generator {
4112 my ( $self, $format ) = ( shift, shift );
4114 my ( $f, $prefix, $suffix, $separator, $column ) =
4115 _condensed_generator_defaults($format);
4117 if ($format eq 'latex') {
4118 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4119 $suffix = "\\\\\n\\hline";
4122 sub { my ($d,$a,$s,$w) = @_;
4123 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4125 } elsif ( $format eq 'html' ) {
4126 $prefix = '<th></th>';
4130 sub { my ($d,$a,$s,$w) = @_;
4131 return qq!<th align="$html_align{$a}">$d</th>!;
4139 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4141 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4144 $prefix. join($separator, @result). $suffix;
4149 sub _condensed_description_generator {
4150 my ( $self, $format ) = ( shift, shift );
4152 my ( $f, $prefix, $suffix, $separator, $column ) =
4153 _condensed_generator_defaults($format);
4155 my $money_char = '$';
4156 if ($format eq 'latex') {
4157 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4159 $separator = " & \n";
4161 sub { my ($d,$a,$s,$w) = @_;
4162 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4164 $money_char = '\\dollar';
4165 }elsif ( $format eq 'html' ) {
4166 $prefix = '"><td align="center"></td>';
4170 sub { my ($d,$a,$s,$w) = @_;
4171 return qq!<td align="$html_align{$a}">$d</td>!;
4173 #$money_char = $conf->config('money_char') || '$';
4174 $money_char = ''; # this is madness
4182 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4184 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4186 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4187 map { $f->{$_}->[$i] } qw(align span width)
4191 $prefix. join( $separator, @result ). $suffix;
4196 sub _condensed_total_generator {
4197 my ( $self, $format ) = ( shift, shift );
4199 my ( $f, $prefix, $suffix, $separator, $column ) =
4200 _condensed_generator_defaults($format);
4203 if ($format eq 'latex') {
4206 $separator = " & \n";
4208 sub { my ($d,$a,$s,$w) = @_;
4209 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4211 }elsif ( $format eq 'html' ) {
4215 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4217 sub { my ($d,$a,$s,$w) = @_;
4218 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4227 # my $r = &{$f->{fields}->[$i]}(@args);
4228 # $r .= ' Total' unless $i;
4230 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4232 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4233 map { $f->{$_}->[$i] } qw(align span width)
4237 $prefix. join( $separator, @result ). $suffix;
4242 =item total_line_generator FORMAT
4244 Returns a coderef used for generation of invoice total line items for this
4245 usage_class. FORMAT is either html or latex
4249 # should not be used: will have issues with hash element names (description vs
4250 # total_item and amount vs total_amount -- another array of functions?
4252 sub _condensed_total_line_generator {
4253 my ( $self, $format ) = ( shift, shift );
4255 my ( $f, $prefix, $suffix, $separator, $column ) =
4256 _condensed_generator_defaults($format);
4259 if ($format eq 'latex') {
4262 $separator = " & \n";
4264 sub { my ($d,$a,$s,$w) = @_;
4265 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4267 }elsif ( $format eq 'html' ) {
4271 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4273 sub { my ($d,$a,$s,$w) = @_;
4274 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4283 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4285 &{$column}( &{$f->{fields}->[$i]}(@args),
4286 map { $f->{$_}->[$i] } qw(align span width)
4290 $prefix. join( $separator, @result ). $suffix;
4295 #sub _items_extra_usage_sections {
4297 # my $escape = shift;
4299 # my %sections = ();
4301 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4302 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4304 # next unless $cust_bill_pkg->pkgnum > 0;
4306 # foreach my $section ( keys %usage_class ) {
4308 # my $usage = $cust_bill_pkg->usage($section);
4310 # next unless $usage && $usage > 0;
4312 # $sections{$section} ||= 0;
4313 # $sections{$section} += $usage;
4319 # map { { 'description' => &{$escape}($_),
4320 # 'subtotal' => $sections{$_},
4321 # 'summarized' => '',
4322 # 'tax_section' => '',
4325 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4329 sub _items_extra_usage_sections {
4331 my $conf = $self->conf;
4339 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4341 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4342 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4343 next unless $cust_bill_pkg->pkgnum > 0;
4345 foreach my $classnum ( keys %usage_class ) {
4346 my $section = $usage_class{$classnum}->classname;
4347 $classnums{$section} = $classnum;
4349 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4350 my $amount = $detail->amount;
4351 next unless $amount && $amount > 0;
4353 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4354 $sections{$section}{amount} += $amount; #subtotal
4355 $sections{$section}{calls}++;
4356 $sections{$section}{duration} += $detail->duration;
4358 my $desc = $detail->regionname;
4359 my $description = $desc;
4360 $description = substr($desc, 0, $maxlength). '...'
4361 if $format eq 'latex' && length($desc) > $maxlength;
4363 $lines{$section}{$desc} ||= {
4364 description => &{$escape}($description),
4365 #pkgpart => $part_pkg->pkgpart,
4366 pkgnum => $cust_bill_pkg->pkgnum,
4371 #unit_amount => $cust_bill_pkg->unitrecur,
4372 quantity => $cust_bill_pkg->quantity,
4373 product_code => 'N/A',
4374 ext_description => [],
4377 $lines{$section}{$desc}{amount} += $amount;
4378 $lines{$section}{$desc}{calls}++;
4379 $lines{$section}{$desc}{duration} += $detail->duration;
4385 my %sectionmap = ();
4386 foreach (keys %sections) {
4387 my $usage_class = $usage_class{$classnums{$_}};
4388 $sectionmap{$_} = { 'description' => &{$escape}($_),
4389 'amount' => $sections{$_}{amount}, #subtotal
4390 'calls' => $sections{$_}{calls},
4391 'duration' => $sections{$_}{duration},
4393 'tax_section' => '',
4394 'sort_weight' => $usage_class->weight,
4395 ( $usage_class->format
4396 ? ( map { $_ => $usage_class->$_($format) }
4397 qw( description_generator header_generator total_generator total_line_generator )
4404 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4408 foreach my $section ( keys %lines ) {
4409 foreach my $line ( keys %{$lines{$section}} ) {
4410 my $l = $lines{$section}{$line};
4411 $l->{section} = $sectionmap{$section};
4412 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4413 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4418 return(\@sections, \@lines);
4424 my $end = $self->_date;
4426 # start at date of previous invoice + 1 second or 0 if no previous invoice
4427 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4428 $start = 0 if !$start;
4431 my $cust_main = $self->cust_main;
4432 my @pkgs = $cust_main->all_pkgs;
4433 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4436 foreach my $pkg ( @pkgs ) {
4437 my @h_cust_svc = $pkg->h_cust_svc($end);
4438 foreach my $h_cust_svc ( @h_cust_svc ) {
4439 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4440 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4442 my $inserted = $h_cust_svc->date_inserted;
4443 my $deleted = $h_cust_svc->date_deleted;
4444 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4446 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4448 # DID either activated or ported in; cannot be both for same DID simultaneously
4449 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4450 && (!$phone_inserted->lnp_status
4451 || $phone_inserted->lnp_status eq ''
4452 || $phone_inserted->lnp_status eq 'native')) {
4455 else { # this one not so clean, should probably move to (h_)svc_phone
4456 my $phone_portedin = qsearchs( 'h_svc_phone',
4457 { 'svcnum' => $h_cust_svc->svcnum,
4458 'lnp_status' => 'portedin' },
4459 FS::h_svc_phone->sql_h_searchs($end),
4461 $num_portedin++ if $phone_portedin;
4464 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4465 if($deleted >= $start && $deleted <= $end && $phone_deleted
4466 && (!$phone_deleted->lnp_status
4467 || $phone_deleted->lnp_status ne 'portingout')) {
4470 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4471 && $phone_deleted->lnp_status
4472 && $phone_deleted->lnp_status eq 'portingout') {
4476 # increment usage minutes
4477 if ( $phone_inserted ) {
4478 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4479 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4482 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4485 # don't look at this service again
4486 push @seen, $h_cust_svc->svcnum;
4490 $minutes = sprintf("%d", $minutes);
4491 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4492 . "$num_deactivated Ported-Out: $num_portedout ",
4493 "Total Minutes: $minutes");
4496 sub _items_accountcode_cdr {
4501 my $section = { 'amount' => 0,
4504 'sort_weight' => '',
4506 'description' => 'Usage by Account Code',
4512 my %accountcodes = ();
4514 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4515 next unless $cust_bill_pkg->pkgnum > 0;
4517 my @header = $cust_bill_pkg->details_header;
4518 next unless scalar(@header);
4519 $section->{'header'} = join(',',@header);
4521 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4523 $section->{'header'} = $detail->formatted('format' => $format)
4524 if($detail->detail eq $section->{'header'});
4526 my $accountcode = $detail->accountcode;
4527 next unless $accountcode;
4529 my $amount = $detail->amount;
4530 next unless $amount && $amount > 0;
4532 $accountcodes{$accountcode} ||= {
4533 description => $accountcode,
4540 product_code => 'N/A',
4541 section => $section,
4542 ext_description => [ $section->{'header'} ],
4546 $section->{'amount'} += $amount;
4547 $accountcodes{$accountcode}{'amount'} += $amount;
4548 $accountcodes{$accountcode}{calls}++;
4549 $accountcodes{$accountcode}{duration} += $detail->duration;
4550 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4554 foreach my $l ( values %accountcodes ) {
4555 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4556 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4557 foreach my $sorted_detail ( @sorted_detail ) {
4558 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4560 delete $l->{detail_temp};
4564 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4566 return ($section,\@sorted_lines);
4569 sub _items_svc_phone_sections {
4571 my $conf = $self->conf;
4579 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4581 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4582 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4584 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4585 next unless $cust_bill_pkg->pkgnum > 0;
4587 my @header = $cust_bill_pkg->details_header;
4588 next unless scalar(@header);
4590 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4592 my $phonenum = $detail->phonenum;
4593 next unless $phonenum;
4595 my $amount = $detail->amount;
4596 next unless $amount && $amount > 0;
4598 $sections{$phonenum} ||= { 'amount' => 0,
4601 'sort_weight' => -1,
4602 'phonenum' => $phonenum,
4604 $sections{$phonenum}{amount} += $amount; #subtotal
4605 $sections{$phonenum}{calls}++;
4606 $sections{$phonenum}{duration} += $detail->duration;
4608 my $desc = $detail->regionname;
4609 my $description = $desc;
4610 $description = substr($desc, 0, $maxlength). '...'
4611 if $format eq 'latex' && length($desc) > $maxlength;
4613 $lines{$phonenum}{$desc} ||= {
4614 description => &{$escape}($description),
4615 #pkgpart => $part_pkg->pkgpart,
4623 product_code => 'N/A',
4624 ext_description => [],
4627 $lines{$phonenum}{$desc}{amount} += $amount;
4628 $lines{$phonenum}{$desc}{calls}++;
4629 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4631 my $line = $usage_class{$detail->classnum}->classname;
4632 $sections{"$phonenum $line"} ||=
4636 'sort_weight' => $usage_class{$detail->classnum}->weight,
4637 'phonenum' => $phonenum,
4638 'header' => [ @header ],
4640 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4641 $sections{"$phonenum $line"}{calls}++;
4642 $sections{"$phonenum $line"}{duration} += $detail->duration;
4644 $lines{"$phonenum $line"}{$desc} ||= {
4645 description => &{$escape}($description),
4646 #pkgpart => $part_pkg->pkgpart,
4654 product_code => 'N/A',
4655 ext_description => [],
4658 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4659 $lines{"$phonenum $line"}{$desc}{calls}++;
4660 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4661 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4662 $detail->formatted('format' => $format);
4667 my %sectionmap = ();
4668 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4669 foreach ( keys %sections ) {
4670 my @header = @{ $sections{$_}{header} || [] };
4672 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4673 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4674 my $usage_class = $summary ? $simple : $usage_simple;
4675 my $ending = $summary ? ' usage charges' : '';
4678 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4680 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4681 'amount' => $sections{$_}{amount}, #subtotal
4682 'calls' => $sections{$_}{calls},
4683 'duration' => $sections{$_}{duration},
4685 'tax_section' => '',
4686 'phonenum' => $sections{$_}{phonenum},
4687 'sort_weight' => $sections{$_}{sort_weight},
4688 'post_total' => $summary, #inspire pagebreak
4690 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4691 qw( description_generator
4694 total_line_generator
4701 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4702 $a->{sort_weight} <=> $b->{sort_weight}
4707 foreach my $section ( keys %lines ) {
4708 foreach my $line ( keys %{$lines{$section}} ) {
4709 my $l = $lines{$section}{$line};
4710 $l->{section} = $sectionmap{$section};
4711 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4712 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4717 if($conf->exists('phone_usage_class_summary')) {
4718 # this only works with Latex
4722 # after this, we'll have only two sections per DID:
4723 # Calls Summary and Calls Detail
4724 foreach my $section ( @sections ) {
4725 if($section->{'post_total'}) {
4726 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4727 $section->{'total_line_generator'} = sub { '' };
4728 $section->{'total_generator'} = sub { '' };
4729 $section->{'header_generator'} = sub { '' };
4730 $section->{'description_generator'} = '';
4731 push @newsections, $section;
4732 my %calls_detail = %$section;
4733 $calls_detail{'post_total'} = '';
4734 $calls_detail{'sort_weight'} = '';
4735 $calls_detail{'description_generator'} = sub { '' };
4736 $calls_detail{'header_generator'} = sub {
4737 return ' & Date/Time & Called Number & Duration & Price'
4738 if $format eq 'latex';
4741 $calls_detail{'description'} = 'Calls Detail: '
4742 . $section->{'phonenum'};
4743 push @newsections, \%calls_detail;
4747 # after this, each usage class is collapsed/summarized into a single
4748 # line under the Calls Summary section
4749 foreach my $newsection ( @newsections ) {
4750 if($newsection->{'post_total'}) { # this means Calls Summary
4751 foreach my $section ( @sections ) {
4752 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4753 && !$section->{'post_total'});
4754 my $newdesc = $section->{'description'};
4755 my $tn = $section->{'phonenum'};
4756 $newdesc =~ s/$tn//g;
4757 my $line = { ext_description => [],
4761 calls => $section->{'calls'},
4762 section => $newsection,
4763 duration => $section->{'duration'},
4764 description => $newdesc,
4765 amount => sprintf("%.2f",$section->{'amount'}),
4766 product_code => 'N/A',
4768 push @newlines, $line;
4773 # after this, Calls Details is populated with all CDRs
4774 foreach my $newsection ( @newsections ) {
4775 if(!$newsection->{'post_total'}) { # this means Calls Details
4776 foreach my $line ( @lines ) {
4777 next unless (scalar(@{$line->{'ext_description'}}) &&
4778 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4780 my @extdesc = @{$line->{'ext_description'}};
4782 foreach my $extdesc ( @extdesc ) {
4783 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4784 push @newextdesc, $extdesc;
4786 $line->{'ext_description'} = \@newextdesc;
4787 $line->{'section'} = $newsection;
4788 push @newlines, $line;
4793 return(\@newsections, \@newlines);
4796 return(\@sections, \@lines);
4800 sub _items { # seems to be unused
4803 #my @display = scalar(@_)
4805 # : qw( _items_previous _items_pkg );
4806 # #: qw( _items_pkg );
4807 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4808 my @display = qw( _items_previous _items_pkg );
4811 foreach my $display ( @display ) {
4812 push @b, $self->$display(@_);
4817 sub _items_previous {
4819 my $conf = $self->conf;
4820 my $cust_main = $self->cust_main;
4821 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4823 foreach ( @pr_cust_bill ) {
4824 my $date = $conf->exists('invoice_show_prior_due_date')
4825 ? 'due '. $_->due_date2str($date_format)
4826 : time2str($date_format, $_->_date);
4828 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4829 #'pkgpart' => 'N/A',
4831 'amount' => sprintf("%.2f", $_->owed),
4837 # 'description' => 'Previous Balance',
4838 # #'pkgpart' => 'N/A',
4839 # 'pkgnum' => 'N/A',
4840 # 'amount' => sprintf("%10.2f", $pr_total ),
4841 # 'ext_description' => [ map {
4842 # "Invoice ". $_->invnum.
4843 # " (". time2str("%x",$_->_date). ") ".
4844 # sprintf("%10.2f", $_->owed)
4845 # } @pr_cust_bill ],
4850 =item _items_pkg [ OPTIONS ]
4852 Return line item hashes for each package item on this invoice. Nearly
4855 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4857 The only OPTIONS accepted is 'section', which may point to a hashref
4858 with a key named 'condensed', which may have a true value. If it
4859 does, this method tries to merge identical items into items with
4860 'quantity' equal to the number of items (not the sum of their
4861 separate quantities, for some reason).
4869 warn "$me _items_pkg searching for all package line items\n"
4872 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4874 warn "$me _items_pkg filtering line items\n"
4876 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4878 if ($options{section} && $options{section}->{condensed}) {
4880 warn "$me _items_pkg condensing section\n"
4884 local $Storable::canonical = 1;
4885 foreach ( @items ) {
4887 delete $item->{ref};
4888 delete $item->{ext_description};
4889 my $key = freeze($item);
4890 $itemshash{$key} ||= 0;
4891 $itemshash{$key} ++; # += $item->{quantity};
4893 @items = sort { $a->{description} cmp $b->{description} }
4894 map { my $i = thaw($_);
4895 $i->{quantity} = $itemshash{$_};
4897 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4903 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4910 return 0 unless $a->itemdesc cmp $b->itemdesc;
4911 return -1 if $b->itemdesc eq 'Tax';
4912 return 1 if $a->itemdesc eq 'Tax';
4913 return -1 if $b->itemdesc eq 'Other surcharges';
4914 return 1 if $a->itemdesc eq 'Other surcharges';
4915 $a->itemdesc cmp $b->itemdesc;
4920 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4921 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4924 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4926 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4927 list of hashrefs describing the line items they generate on the invoice.
4929 OPTIONS may include:
4931 format: the invoice format.
4933 escape_function: the function used to escape strings.
4935 DEPRECATED? (expensive, mostly unused?)
4936 format_function: the function used to format CDRs.
4938 section: a hashref containing 'description'; if this is present,
4939 cust_bill_pkg_display records not belonging to this section are
4942 multisection: a flag indicating that this is a multisection invoice,
4943 which does something complicated.
4945 multilocation: a flag to display the location label for the package.
4947 Returns a list of hashrefs, each of which may contain:
4949 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4950 ext_description, which is an arrayref of detail lines to show below
4955 sub _items_cust_bill_pkg {
4957 my $conf = $self->conf;
4958 my $cust_bill_pkgs = shift;
4961 my $format = $opt{format} || '';
4962 my $escape_function = $opt{escape_function} || sub { shift };
4963 my $format_function = $opt{format_function} || '';
4964 my $no_usage = $opt{no_usage} || '';
4965 my $unsquelched = $opt{unsquelched} || ''; #unused
4966 my $section = $opt{section}->{description} if $opt{section};
4967 my $summary_page = $opt{summary_page} || ''; #unused
4968 my $multilocation = $opt{multilocation} || '';
4969 my $multisection = $opt{multisection} || '';
4970 my $discount_show_always = 0;
4972 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4974 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4977 my ($s, $r, $u) = ( undef, undef, undef );
4978 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4981 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4982 if ( $_ && !$cust_bill_pkg->hidden ) {
4983 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4984 $_->{amount} =~ s/^\-0\.00$/0.00/;
4985 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4987 if $_->{amount} != 0
4988 || $discount_show_always
4989 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4990 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4996 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4998 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4999 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
5002 foreach my $display ( grep { defined($section)
5003 ? $_->section eq $section
5006 #grep { !$_->summary || !$summary_page } # bunk!
5007 grep { !$_->summary || $multisection }
5008 @cust_bill_pkg_display
5012 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5013 $display->billpkgdisplaynum. "\n"
5016 my $type = $display->type;
5018 my $desc = $cust_bill_pkg->desc;
5019 $desc = substr($desc, 0, $maxlength). '...'
5020 if $format eq 'latex' && length($desc) > $maxlength;
5022 my %details_opt = ( 'format' => $format,
5023 'escape_function' => $escape_function,
5024 'format_function' => $format_function,
5025 'no_usage' => $opt{'no_usage'},
5028 if ( $cust_bill_pkg->pkgnum > 0 ) {
5030 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5033 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5035 # which pkgpart to show for display purposes?
5036 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5038 # start/end dates for invoice formats that do nonstandard
5040 my %item_dates = ();
5041 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5042 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5044 if ( (!$type || $type eq 'S')
5045 && ( $cust_bill_pkg->setup != 0
5046 || $cust_bill_pkg->setup_show_zero
5051 warn "$me _items_cust_bill_pkg adding setup\n"
5054 my $description = $desc;
5055 $description .= ' Setup'
5056 if $cust_bill_pkg->recur != 0
5057 || $discount_show_always
5058 || $cust_bill_pkg->recur_show_zero;
5062 unless ( $cust_pkg->part_pkg->hide_svc_detail
5063 || $cust_bill_pkg->hidden )
5066 my @svc_labels = map &{$escape_function}($_),
5067 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5068 push @d, @svc_labels
5069 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5070 $svc_label = $svc_labels[0];
5072 if ( $multilocation ) {
5073 my $loc = $cust_pkg->location_label;
5074 $loc = substr($loc, 0, $maxlength). '...'
5075 if $format eq 'latex' && length($loc) > $maxlength;
5076 push @d, &{$escape_function}($loc);
5079 } #unless hiding service details
5081 push @d, $cust_bill_pkg->details(%details_opt)
5082 if $cust_bill_pkg->recur == 0;
5084 if ( $cust_bill_pkg->hidden ) {
5085 $s->{amount} += $cust_bill_pkg->setup;
5086 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5087 push @{ $s->{ext_description} }, @d;
5091 description => $description,
5092 pkgpart => $pkgpart,
5093 pkgnum => $cust_bill_pkg->pkgnum,
5094 amount => $cust_bill_pkg->setup,
5095 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5096 unit_amount => $cust_bill_pkg->unitsetup,
5097 quantity => $cust_bill_pkg->quantity,
5098 ext_description => \@d,
5099 svc_label => ($svc_label || ''),
5105 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5107 $cust_bill_pkg->recur != 0
5108 || $cust_bill_pkg->setup == 0
5109 || $discount_show_always
5110 || $cust_bill_pkg->recur_show_zero
5115 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5118 my $is_summary = $display->summary;
5119 my $description = ($is_summary && $type && $type eq 'U')
5120 ? "Usage charges" : $desc;
5122 my $part_pkg = $cust_pkg->part_pkg;
5124 #pry be a bit more efficient to look some of this conf stuff up
5127 $conf->exists('disable_line_item_date_ranges')
5128 || $part_pkg->option('disable_line_item_date_ranges',1)
5129 || ! $cust_bill_pkg->sdate
5130 || ! $cust_bill_pkg->edate
5133 my $date_style = '';
5134 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5135 $cust_main->agentnum
5137 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5138 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5139 $cust_main->agentnum
5141 if ( defined($date_style) && $date_style eq 'month_of' ) {
5142 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5143 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5144 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5145 $cust_main->agentnum
5147 $desc .= ' ' unless $desc =~ /\s$/;
5148 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5150 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5151 " - ". time2str($date_format, $cust_bill_pkg->edate);
5153 $description .= " ($time_period)";
5157 my @seconds = (); # for display of usage info
5160 #at least until cust_bill_pkg has "past" ranges in addition to
5161 #the "future" sdate/edate ones... see #3032
5162 my @dates = ( $self->_date );
5163 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5164 push @dates, $prev->sdate if $prev;
5165 push @dates, undef if !$prev;
5167 unless ( $cust_pkg->part_pkg->hide_svc_detail
5168 || $cust_bill_pkg->itemdesc
5169 || $cust_bill_pkg->hidden
5170 || $is_summary && $type && $type eq 'U' )
5173 warn "$me _items_cust_bill_pkg adding service details\n"
5176 my @svc_labels = map &{$escape_function}($_),
5177 $cust_pkg->h_labels_short(@dates, 'I');
5178 push @d, @svc_labels
5179 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5180 $svc_label = $svc_labels[0];
5182 warn "$me _items_cust_bill_pkg done adding service details\n"
5185 if ( $multilocation ) {
5186 my $loc = $cust_pkg->location_label;
5187 $loc = substr($loc, 0, $maxlength). '...'
5188 if $format eq 'latex' && length($loc) > $maxlength;
5189 push @d, &{$escape_function}($loc);
5192 # Display of seconds_since_sqlradacct:
5193 # On the invoice, when processing @detail_items, look for a field
5194 # named 'seconds'. This will contain total seconds for each
5195 # service, in the same order as @ext_description. For services
5196 # that don't support this it will show undef.
5197 if ( $conf->exists('svc_acct-usage_seconds')
5198 and ! $cust_bill_pkg->pkgpart_override ) {
5199 foreach my $cust_svc (
5200 $cust_pkg->h_cust_svc(@dates, 'I')
5203 # eval because not having any part_export_usage exports
5204 # is a fatal error, last_bill/_date because that's how
5205 # sqlradius_hour billing does it
5207 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5209 push @seconds, $sec;
5211 } #if svc_acct-usage_seconds
5215 unless ( $is_summary ) {
5216 warn "$me _items_cust_bill_pkg adding details\n"
5219 #instead of omitting details entirely in this case (unwanted side
5220 # effects), just omit CDRs
5221 $details_opt{'no_usage'} = 1
5222 if $type && $type eq 'R';
5224 push @d, $cust_bill_pkg->details(%details_opt);
5227 warn "$me _items_cust_bill_pkg calculating amount\n"
5232 $amount = $cust_bill_pkg->recur;
5233 } elsif ($type eq 'R') {
5234 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5235 } elsif ($type eq 'U') {
5236 $amount = $cust_bill_pkg->usage;
5239 if ( !$type || $type eq 'R' ) {
5241 warn "$me _items_cust_bill_pkg adding recur\n"
5244 if ( $cust_bill_pkg->hidden ) {
5245 $r->{amount} += $amount;
5246 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5247 push @{ $r->{ext_description} }, @d;
5250 description => $description,
5251 pkgpart => $pkgpart,
5252 pkgnum => $cust_bill_pkg->pkgnum,
5254 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5255 unit_amount => $cust_bill_pkg->unitrecur,
5256 quantity => $cust_bill_pkg->quantity,
5258 ext_description => \@d,
5259 svc_label => ($svc_label || ''),
5261 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5264 } else { # $type eq 'U'
5266 warn "$me _items_cust_bill_pkg adding usage\n"
5269 if ( $cust_bill_pkg->hidden ) {
5270 $u->{amount} += $amount;
5271 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5272 push @{ $u->{ext_description} }, @d;
5275 description => $description,
5276 pkgpart => $pkgpart,
5277 pkgnum => $cust_bill_pkg->pkgnum,
5279 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5280 unit_amount => $cust_bill_pkg->unitrecur,
5281 quantity => $cust_bill_pkg->quantity,
5283 ext_description => \@d,
5288 } # recurring or usage with recurring charge
5290 } else { #pkgnum tax or one-shot line item (??)
5292 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5295 if ( $cust_bill_pkg->setup != 0 ) {
5297 'description' => $desc,
5298 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5301 if ( $cust_bill_pkg->recur != 0 ) {
5303 'description' => "$desc (".
5304 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5305 time2str($date_format, $cust_bill_pkg->edate). ')',
5306 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5314 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5315 && $conf->exists('discount-show-always'));
5319 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5321 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5322 $_->{amount} =~ s/^\-0\.00$/0.00/;
5323 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5325 if $_->{amount} != 0
5326 || $discount_show_always
5327 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5328 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5332 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5339 sub _items_credits {
5340 my( $self, %opt ) = @_;
5341 my $trim_len = $opt{'trim_len'} || 60;
5346 if ( $self->conf->exists('previous_balance-payments_since') ) {
5347 if ( $opt{'template'} eq 'statement' ) {
5348 # then the current bill is a "statement" (i.e. an invoice sent as
5349 # a payment receipt)
5350 # and in that case we want to see payments on or after THIS invoice
5351 @objects = qsearch('cust_credit', {
5352 'custnum' => $self->custnum,
5353 '_date' => {op => '>=', value => $self->_date},
5357 $date = $self->previous_bill->_date if $self->previous_bill;
5358 @objects = qsearch('cust_credit', {
5359 'custnum' => $self->custnum,
5360 '_date' => {op => '>=', value => $date},
5364 @objects = $self->cust_credited;
5367 foreach my $obj ( @objects ) {
5368 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5370 my $reason = substr($cust_credit->reason, 0, $trim_len);
5371 $reason .= '...' if length($reason) < length($cust_credit->reason);
5372 $reason = " ($reason) " if $reason;
5375 #'description' => 'Credit ref\#'. $_->crednum.
5376 # " (". time2str("%x",$_->cust_credit->_date) .")".
5378 'description' => $self->mt('Credit applied').' '.
5379 time2str($date_format,$obj->_date). $reason,
5380 'amount' => sprintf("%.2f",$obj->amount),
5388 sub _items_payments {
5393 my $detailed = $self->conf->exists('invoice_payment_details');
5395 if ( $self->conf->exists('previous_balance-payments_since') ) {
5396 # then show payments dated on/after the previous bill...
5397 if ( $opt{'template'} eq 'statement' ) {
5398 # then the current bill is a "statement" (i.e. an invoice sent as
5399 # a payment receipt)
5400 # and in that case we want to see payments on or after THIS invoice
5401 @objects = qsearch('cust_pay', {
5402 'custnum' => $self->custnum,
5403 '_date' => {op => '>=', value => $self->_date},
5406 # the normal case: payments on or after the previous invoice
5408 $date = $self->previous_bill->_date if $self->previous_bill;
5409 @objects = qsearch('cust_pay', {
5410 'custnum' => $self->custnum,
5411 '_date' => {op => '>=', value => $date},
5413 # and before the current bill...
5414 @objects = grep { $_->_date < $self->_date } @objects;
5417 @objects = $self->cust_bill_pay;
5420 foreach my $obj (@objects) {
5421 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5422 my $desc = $self->mt('Payment received').' '.
5423 time2str($date_format, $cust_pay->_date );
5424 $desc .= $self->mt(' via ') .
5425 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5429 'description' => $desc,
5430 'amount' => sprintf("%.2f", $obj->amount )
5438 =item _items_discounts_avail
5440 Returns an array of line item hashrefs representing available term discounts
5441 for this invoice. This makes the same assumptions that apply to term
5442 discounts in general: that the package is billed monthly, at a flat rate,
5443 with no usage charges. A prorated first month will be handled, as will
5444 a setup fee if the discount is allowed to apply to setup fees.
5448 sub _items_discounts_avail {
5450 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5452 my %plans = $self->discount_plans;
5454 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5458 my $plan = $plans{$months};
5460 my $term_total = sprintf('%.2f', $plan->discounted_total);
5461 my $percent = sprintf('%.0f',
5462 100 * (1 - $term_total / $plan->base_total) );
5463 my $permonth = sprintf('%.2f', $term_total / $months);
5464 my $detail = $self->mt('discount on item'). ' '.
5465 join(', ', map { "#$_" } $plan->pkgnums)
5468 # discounts for non-integer months don't work anyway
5469 $months = sprintf("%d", $months);
5472 description => $self->mt('Save [_1]% by paying for [_2] months',
5474 amount => $self->mt('[_1] ([_2] per month)',
5475 $term_total, $money_char.$permonth),
5476 ext_description => ($detail || ''),
5479 sort { $b <=> $a } keys %plans;
5483 =item call_details [ OPTION => VALUE ... ]
5485 Returns an array of CSV strings representing the call details for this invoice
5486 The only option available is the boolean prepend_billed_number
5491 my ($self, %opt) = @_;
5493 my $format_function = sub { shift };
5495 if ($opt{prepend_billed_number}) {
5496 $format_function = sub {
5500 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5505 my @details = map { $_->details( 'format_function' => $format_function,
5506 'escape_function' => sub{ return() },
5510 $self->cust_bill_pkg;
5511 my $header = $details[0];
5512 ( $header, grep { $_ ne $header } @details );
5522 =item process_reprint
5526 sub process_reprint {
5527 process_re_X('print', @_);
5530 =item process_reemail
5534 sub process_reemail {
5535 process_re_X('email', @_);
5543 process_re_X('fax', @_);
5551 process_re_X('ftp', @_);
5558 sub process_respool {
5559 process_re_X('spool', @_);
5562 use Storable qw(thaw);
5566 my( $method, $job ) = ( shift, shift );
5567 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5569 my $param = thaw(decode_base64(shift));
5570 warn Dumper($param) if $DEBUG;
5581 my($method, $job, %param ) = @_;
5583 warn "re_X $method for job $job with param:\n".
5584 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5587 #some false laziness w/search/cust_bill.html
5589 my $orderby = 'ORDER BY cust_bill._date';
5591 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5593 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5595 my @cust_bill = qsearch( {
5596 #'select' => "cust_bill.*",
5597 'table' => 'cust_bill',
5598 'addl_from' => $addl_from,
5600 'extra_sql' => $extra_sql,
5601 'order_by' => $orderby,
5605 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5607 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5610 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5611 foreach my $cust_bill ( @cust_bill ) {
5612 $cust_bill->$method();
5614 if ( $job ) { #progressbar foo
5616 if ( time - $min_sec > $last ) {
5617 my $error = $job->update_statustext(
5618 int( 100 * $num / scalar(@cust_bill) )
5620 die $error if $error;
5631 =head1 CLASS METHODS
5637 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5642 my ($class, $start, $end) = @_;
5644 $class->paid_sql($start, $end). ' - '.
5645 $class->credited_sql($start, $end);
5650 Returns an SQL fragment to retreive the net amount (charged minus credited).
5655 my ($class, $start, $end) = @_;
5656 'charged - '. $class->credited_sql($start, $end);
5661 Returns an SQL fragment to retreive the amount paid against this invoice.
5666 my ($class, $start, $end) = @_;
5667 $start &&= "AND cust_bill_pay._date <= $start";
5668 $end &&= "AND cust_bill_pay._date > $end";
5669 $start = '' unless defined($start);
5670 $end = '' unless defined($end);
5671 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5672 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5677 Returns an SQL fragment to retreive the amount credited against this invoice.
5682 my ($class, $start, $end) = @_;
5683 $start &&= "AND cust_credit_bill._date <= $start";
5684 $end &&= "AND cust_credit_bill._date > $end";
5685 $start = '' unless defined($start);
5686 $end = '' unless defined($end);
5687 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5688 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5693 Returns an SQL fragment to retrieve the due date of an invoice.
5694 Currently only supported on PostgreSQL.
5699 my $conf = new FS::Conf;
5703 cust_bill.invoice_terms,
5704 cust_main.invoice_terms,
5705 \''.($conf->config('invoice_default_terms') || '').'\'
5706 ), E\'Net (\\\\d+)\'
5708 ) * 86400 + cust_bill._date'
5711 =item search_sql_where HASHREF
5713 Class method which returns an SQL WHERE fragment to search for parameters
5714 specified in HASHREF. Valid parameters are
5720 List reference of start date, end date, as UNIX timestamps.
5730 List reference of charged limits (exclusive).
5734 List reference of charged limits (exclusive).
5738 flag, return open invoices only
5742 flag, return net invoices only
5746 =item newest_percust
5750 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5754 sub search_sql_where {
5755 my($class, $param) = @_;
5757 warn "$me search_sql_where called with params: \n".
5758 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5764 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5765 push @search, "cust_main.agentnum = $1";
5769 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5770 push @search, "cust_main.refnum = $1";
5774 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5775 push @search, "cust_bill.custnum = $1";
5779 if ( $param->{'cust_classnum'} ) {
5780 my $classnums = $param->{'cust_classnum'};
5781 $classnums = [ $classnums ] if !ref($classnums);
5782 $classnums = [ grep /^\d+$/, @$classnums ];
5783 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5788 if ( $param->{_date} ) {
5789 my($beginning, $ending) = @{$param->{_date}};
5791 push @search, "cust_bill._date >= $beginning",
5792 "cust_bill._date < $ending";
5796 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5797 push @search, "cust_bill.invnum >= $1";
5799 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5800 push @search, "cust_bill.invnum <= $1";
5804 if ( $param->{charged} ) {
5805 my @charged = ref($param->{charged})
5806 ? @{ $param->{charged} }
5807 : ($param->{charged});
5809 push @search, map { s/^charged/cust_bill.charged/; $_; }
5813 my $owed_sql = FS::cust_bill->owed_sql;
5816 if ( $param->{owed} ) {
5817 my @owed = ref($param->{owed})
5818 ? @{ $param->{owed} }
5820 push @search, map { s/^owed/$owed_sql/; $_; }
5825 push @search, "0 != $owed_sql"
5826 if $param->{'open'};
5827 push @search, '0 != '. FS::cust_bill->net_sql
5831 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5832 if $param->{'days'};
5835 if ( $param->{'newest_percust'} ) {
5837 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5838 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5840 my @newest_where = map { my $x = $_;
5841 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5844 grep ! /^cust_main./, @search;
5845 my $newest_where = scalar(@newest_where)
5846 ? ' AND '. join(' AND ', @newest_where)
5850 push @search, "cust_bill._date = (
5851 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5852 WHERE newest_cust_bill.custnum = cust_bill.custnum
5858 #promised_date - also has an option to accept nulls
5859 if ( $param->{promised_date} ) {
5860 my($beginning, $ending, $null) = @{$param->{promised_date}};
5862 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5863 "cust_bill.promised_date < $ending )" .
5864 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5867 #agent virtualization
5868 my $curuser = $FS::CurrentUser::CurrentUser;
5869 if ( $curuser->username eq 'fs_queue'
5870 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5872 my $newuser = qsearchs('access_user', {
5873 'username' => $username,
5877 $curuser = $newuser;
5879 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5882 push @search, $curuser->agentnums_sql;
5884 join(' AND ', @search );
5896 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5897 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base