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, $self->_items_credits ) ;
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
3005 my $tax_weight = _pkg_category($tax_section->{description})
3006 ? _pkg_category($tax_section->{description})->weight
3008 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3009 $tax_section->{'sort_weight'} = $tax_weight;
3012 my $adjusttotal = 0;
3013 my $adjust_section = {
3014 'description' => $self->mt('Credits, Payments, and Adjustments'),
3015 'adjust_section' => 1,
3016 'subtotal' => 0, # adjusted below
3018 my $adjust_weight = _pkg_category($adjust_section->{description})
3019 ? _pkg_category($adjust_section->{description})->weight
3021 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3022 $adjust_section->{'sort_weight'} = $adjust_weight;
3024 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3025 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3026 $invoice_data{'multisection'} = $multisection;
3027 my $late_sections = [];
3028 my $extra_sections = [];
3029 my $extra_lines = ();
3031 my $default_section = { 'description' => '',
3036 if ( $multisection ) {
3037 ($extra_sections, $extra_lines) =
3038 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3039 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3041 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3043 push @detail_items, @$extra_lines if $extra_lines;
3045 $self->_items_sections( $late_sections, # this could stand a refactor
3047 $escape_function_nonbsp,
3051 if ($conf->exists('svc_phone_sections')) {
3052 my ($phone_sections, $phone_lines) =
3053 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3054 push @{$late_sections}, @$phone_sections;
3055 push @detail_items, @$phone_lines;
3057 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3058 my ($accountcode_section, $accountcode_lines) =
3059 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3060 if ( scalar(@$accountcode_lines) ) {
3061 push @{$late_sections}, $accountcode_section;
3062 push @detail_items, @$accountcode_lines;
3065 } else {# not multisection
3066 # make a default section
3067 push @sections, $default_section;
3068 # and calculate the finance charge total, since it won't get done otherwise.
3069 # XXX possibly other totals?
3070 # XXX possibly finance_pkgclass should not be used in this manner?
3071 if ( $conf->exists('finance_pkgclass') ) {
3072 my @finance_charges;
3073 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3074 if ( grep { $_->section eq $invoice_data{finance_section} }
3075 $cust_bill_pkg->cust_bill_pkg_display ) {
3076 # I think these are always setup fees, but just to be sure...
3077 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3080 $invoice_data{finance_amount} =
3081 sprintf('%.2f', sum( @finance_charges ) || 0);
3085 # previous invoice balances in the Previous Charges section if there
3086 # is one, otherwise in the main detail section
3087 if ( $self->can('_items_previous') &&
3088 $self->enable_previous &&
3089 ! $conf->exists('previous_balance-summary_only') ) {
3091 warn "$me adding previous balances\n"
3094 foreach my $line_item ( $self->_items_previous ) {
3097 ext_description => [],
3099 $detail->{'ref'} = $line_item->{'pkgnum'};
3100 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3101 $detail->{'quantity'} = 1;
3102 $detail->{'section'} = $multisection ? $previous_section
3104 $detail->{'description'} = &$escape_function($line_item->{'description'});
3105 if ( exists $line_item->{'ext_description'} ) {
3106 @{$detail->{'ext_description'}} = map {
3107 &$escape_function($_);
3108 } @{$line_item->{'ext_description'}};
3110 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3111 $line_item->{'amount'};
3112 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3114 push @detail_items, $detail;
3115 push @buf, [ $detail->{'description'},
3116 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3122 if ( @pr_cust_bill && $self->enable_previous ) {
3123 push @buf, ['','-----------'];
3124 push @buf, [ $self->mt('Total Previous Balance'),
3125 $money_char. sprintf("%10.2f", $pr_total) ];
3129 if ( $conf->exists('svc_phone-did-summary') ) {
3130 warn "$me adding DID summary\n"
3133 my ($didsummary,$minutes) = $self->_did_summary;
3134 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3136 { 'description' => $didsummary_desc,
3137 'ext_description' => [ $didsummary, $minutes ],
3141 foreach my $section (@sections, @$late_sections) {
3143 warn "$me adding section \n". Dumper($section)
3146 # begin some normalization
3147 $section->{'subtotal'} = $section->{'amount'}
3149 && !exists($section->{subtotal})
3150 && exists($section->{amount});
3152 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3153 if ( $invoice_data{finance_section} &&
3154 $section->{'description'} eq $invoice_data{finance_section} );
3156 $section->{'subtotal'} = $other_money_char.
3157 sprintf('%.2f', $section->{'subtotal'})
3160 # continue some normalization
3161 $section->{'amount'} = $section->{'subtotal'}
3165 if ( $section->{'description'} ) {
3166 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3171 warn "$me setting options\n"
3174 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3176 $options{'section'} = $section if $multisection;
3177 $options{'format'} = $format;
3178 $options{'escape_function'} = $escape_function;
3179 $options{'no_usage'} = 1 unless $unsquelched;
3180 $options{'unsquelched'} = $unsquelched;
3181 $options{'summary_page'} = $summarypage;
3182 $options{'skip_usage'} =
3183 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3184 $options{'multilocation'} = $multilocation;
3185 $options{'multisection'} = $multisection;
3187 warn "$me searching for line items\n"
3190 foreach my $line_item ( $self->_items_pkg(%options) ) {
3192 warn "$me adding line item $line_item\n"
3196 ext_description => [],
3198 $detail->{'ref'} = $line_item->{'pkgnum'};
3199 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3200 $detail->{'quantity'} = $line_item->{'quantity'};
3201 $detail->{'section'} = $section;
3202 $detail->{'description'} = &$escape_function($line_item->{'description'});
3203 if ( exists $line_item->{'ext_description'} ) {
3204 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3206 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3207 $line_item->{'amount'};
3208 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3209 $line_item->{'unit_amount'};
3210 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3212 $detail->{'sdate'} = $line_item->{'sdate'};
3213 $detail->{'edate'} = $line_item->{'edate'};
3214 $detail->{'seconds'} = $line_item->{'seconds'};
3215 $detail->{'svc_label'} = $line_item->{'svc_label'};
3217 push @detail_items, $detail;
3218 push @buf, ( [ $detail->{'description'},
3219 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3221 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3225 if ( $section->{'description'} ) {
3226 push @buf, ( ['','-----------'],
3227 [ $section->{'description'}. ' sub-total',
3228 $section->{'subtotal'} # already formatted this
3237 $invoice_data{current_less_finance} =
3238 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3240 # create a major section for previous balance if we have major sections,
3241 # or if previous_section is in summary form
3242 if ( ( $multisection && $self->enable_previous )
3243 || $conf->exists('previous_balance-summary_only') )
3245 unshift @sections, $previous_section if $pr_total;
3248 warn "$me adding taxes\n"
3251 foreach my $tax ( $self->_items_tax ) {
3253 $taxtotal += $tax->{'amount'};
3255 my $description = &$escape_function( $tax->{'description'} );
3256 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3258 if ( $multisection ) {
3260 my $money = $old_latex ? '' : $money_char;
3261 push @detail_items, {
3262 ext_description => [],
3265 description => $description,
3266 amount => $money. $amount,
3268 section => $tax_section,
3273 push @total_items, {
3274 'total_item' => $description,
3275 'total_amount' => $other_money_char. $amount,
3280 push @buf,[ $description,
3281 $money_char. $amount,
3288 $total->{'total_item'} = $self->mt('Sub-total');
3289 $total->{'total_amount'} =
3290 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3292 if ( $multisection ) {
3293 $tax_section->{'subtotal'} = $other_money_char.
3294 sprintf('%.2f', $taxtotal);
3295 $tax_section->{'pretotal'} = 'New charges sub-total '.
3296 $total->{'total_amount'};
3297 push @sections, $tax_section if $taxtotal;
3299 unshift @total_items, $total;
3302 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3304 push @buf,['','-----------'];
3305 push @buf,[$self->mt(
3306 (!$self->enable_previous)
3308 : 'Total New Charges'
3310 $money_char. sprintf("%10.2f",$self->charged) ];
3313 # calculate total, possibly including total owed on previous
3318 $item = $conf->config('previous_balance-exclude_from_total')
3319 || 'Total New Charges'
3320 if $conf->exists('previous_balance-exclude_from_total');
3321 my $amount = $self->charged;
3322 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3323 $amount += $pr_total;
3326 $total->{'total_item'} = &$embolden_function($self->mt($item));
3327 $total->{'total_amount'} =
3328 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3329 if ( $multisection ) {
3330 if ( $adjust_section->{'sort_weight'} ) {
3331 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3332 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3334 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3335 $other_money_char. sprintf('%.2f', $self->charged );
3338 push @total_items, $total;
3340 push @buf,['','-----------'];
3343 sprintf( '%10.2f', $amount )
3348 # if we're showing previous invoices, also show previous
3349 # credits and payments
3350 if ( $self->enable_previous
3351 and $self->can('_items_credits')
3352 and $self->can('_items_payments') )
3354 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3357 my $credittotal = 0;
3358 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3361 $total->{'total_item'} = &$escape_function($credit->{'description'});
3362 $credittotal += $credit->{'amount'};
3363 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3364 $adjusttotal += $credit->{'amount'};
3365 if ( $multisection ) {
3366 my $money = $old_latex ? '' : $money_char;
3367 push @detail_items, {
3368 ext_description => [],
3371 description => &$escape_function($credit->{'description'}),
3372 amount => $money. $credit->{'amount'},
3374 section => $adjust_section,
3377 push @total_items, $total;
3381 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3384 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3385 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3389 my $paymenttotal = 0;
3390 foreach my $payment ( $self->_items_payments ) {
3392 $total->{'total_item'} = &$escape_function($payment->{'description'});
3393 $paymenttotal += $payment->{'amount'};
3394 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3395 $adjusttotal += $payment->{'amount'};
3396 if ( $multisection ) {
3397 my $money = $old_latex ? '' : $money_char;
3398 push @detail_items, {
3399 ext_description => [],
3402 description => &$escape_function($payment->{'description'}),
3403 amount => $money. $payment->{'amount'},
3405 section => $adjust_section,
3408 push @total_items, $total;
3410 push @buf, [ $payment->{'description'},
3411 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3414 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3416 if ( $multisection ) {
3417 $adjust_section->{'subtotal'} = $other_money_char.
3418 sprintf('%.2f', $adjusttotal);
3419 push @sections, $adjust_section
3420 unless $adjust_section->{sort_weight};
3423 # create Balance Due message
3426 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3427 $total->{'total_amount'} =
3428 &$embolden_function(
3429 $other_money_char. sprintf('%.2f', #why? $summarypage
3430 # ? $self->charged +
3431 # $self->billing_balance
3433 $self->owed + $pr_total
3436 if ( $multisection && !$adjust_section->{sort_weight} ) {
3437 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3438 $total->{'total_amount'};
3440 push @total_items, $total;
3442 push @buf,['','-----------'];
3443 push @buf,[$self->balance_due_msg, $money_char.
3444 sprintf("%10.2f", $balance_due ) ];
3447 if ( $conf->exists('previous_balance-show_credit')
3448 and $cust_main->balance < 0 ) {
3449 my $credit_total = {
3450 'total_item' => &$embolden_function($self->credit_balance_msg),
3451 'total_amount' => &$embolden_function(
3452 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3455 if ( $multisection ) {
3456 $adjust_section->{'posttotal'} .= $newline_token .
3457 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3460 push @total_items, $credit_total;
3462 push @buf,['','-----------'];
3463 push @buf,[$self->credit_balance_msg, $money_char.
3464 sprintf("%10.2f", -$cust_main->balance ) ];
3468 if ( $multisection ) {
3469 if ($conf->exists('svc_phone_sections')) {
3471 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3472 $total->{'total_amount'} =
3473 &$embolden_function(
3474 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3476 my $last_section = pop @sections;
3477 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3478 $total->{'total_amount'};
3479 push @sections, $last_section;
3481 push @sections, @$late_sections
3485 # make a discounts-available section, even without multisection
3486 if ( $conf->exists('discount-show_available')
3487 and my @discounts_avail = $self->_items_discounts_avail ) {
3488 my $discount_section = {
3489 'description' => $self->mt('Discounts Available'),
3494 push @sections, $discount_section;
3495 push @detail_items, map { +{
3496 'ref' => '', #should this be something else?
3497 'section' => $discount_section,
3498 'description' => &$escape_function( $_->{description} ),
3499 'amount' => $money_char . &$escape_function( $_->{amount} ),
3500 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3501 } } @discounts_avail;
3504 # debugging hook: call this with 'diag' => 1 to just get a hash of
3505 # the invoice variables
3506 return \%invoice_data if ( $params{'diag'} );
3508 # All sections and items are built; now fill in templates.
3509 my @includelist = ();
3510 push @includelist, 'summary' if $summarypage;
3511 foreach my $include ( @includelist ) {
3513 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3516 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3518 @inc_src = $conf->config($inc_file, $agentnum);
3522 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3524 my $convert_map = $convert_maps{$format}{$include};
3526 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3527 s/--\@\]/$delimiters{$format}[1]/g;
3530 &$convert_map( $conf->config($inc_file, $agentnum) );
3534 my $inc_tt = new Text::Template (
3536 SOURCE => [ map "$_\n", @inc_src ],
3537 DELIMITERS => $delimiters{$format},
3538 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3540 unless ( $inc_tt->compile() ) {
3541 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3542 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3546 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3548 $invoice_data{$include} =~ s/\n+$//
3549 if ($format eq 'latex');
3554 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3555 /invoice_lines\((\d*)\)/;
3556 $invoice_lines += $1 || scalar(@buf);
3559 die "no invoice_lines() functions in template?"
3560 if ( $format eq 'template' && !$wasfunc );
3562 if ($format eq 'template') {
3564 if ( $invoice_lines ) {
3565 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3566 $invoice_data{'total_pages'}++
3567 if scalar(@buf) % $invoice_lines;
3570 #setup subroutine for the template
3571 $invoice_data{invoice_lines} = sub {
3572 my $lines = shift || scalar(@buf);
3584 push @collect, split("\n",
3585 $text_template->fill_in( HASH => \%invoice_data )
3587 $invoice_data{'page'}++;
3589 map "$_\n", @collect;
3591 # this is where we actually create the invoice
3592 warn "filling in template for invoice ". $self->invnum. "\n"
3594 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3597 $text_template->fill_in(HASH => \%invoice_data);
3601 # helper routine for generating date ranges
3602 sub _prior_month30s {
3605 [ 1, 2592000 ], # 0-30 days ago
3606 [ 2592000, 5184000 ], # 30-60 days ago
3607 [ 5184000, 7776000 ], # 60-90 days ago
3608 [ 7776000, 0 ], # 90+ days ago
3611 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3612 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3617 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3619 Returns an postscript invoice, as a scalar.
3621 Options can be passed as a hashref (recommended) or as a list of time, template
3622 and then any key/value pairs for any other options.
3624 I<time> an optional value used to control the printing of overdue messages. The
3625 default is now. It isn't the date of the invoice; that's the `_date' field.
3626 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3627 L<Time::Local> and L<Date::Parse> for conversion functions.
3629 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3636 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3637 my $ps = generate_ps($file);
3639 unlink($barcodefile) if $barcodefile;
3644 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3646 Returns an PDF invoice, as a scalar.
3648 Options can be passed as a hashref (recommended) or as a list of time, template
3649 and then any key/value pairs for any other options.
3651 I<time> an optional value used to control the printing of overdue messages. The
3652 default is now. It isn't the date of the invoice; that's the `_date' field.
3653 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3654 L<Time::Local> and L<Date::Parse> for conversion functions.
3656 I<template>, if specified, is the name of a suffix for alternate invoices.
3658 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3665 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3666 my $pdf = generate_pdf($file);
3668 unlink($barcodefile) if $barcodefile;
3673 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3675 Returns an HTML invoice, as a scalar.
3677 I<time> an optional value used to control the printing of overdue messages. The
3678 default is now. It isn't the date of the invoice; that's the `_date' field.
3679 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3680 L<Time::Local> and L<Date::Parse> for conversion functions.
3682 I<template>, if specified, is the name of a suffix for alternate invoices.
3684 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3686 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3687 when emailing the invoice as part of a multipart/related MIME email.
3695 %params = %{ shift() };
3697 $params{'time'} = shift;
3698 $params{'template'} = shift;
3699 $params{'cid'} = shift;
3702 $params{'format'} = 'html';
3704 $self->print_generic( %params );
3707 # quick subroutine for print_latex
3709 # There are ten characters that LaTeX treats as special characters, which
3710 # means that they do not simply typeset themselves:
3711 # # $ % & ~ _ ^ \ { }
3713 # TeX ignores blanks following an escaped character; if you want a blank (as
3714 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3718 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3719 $value =~ s/([<>])/\$$1\$/g;
3725 encode_entities($value);
3729 sub _html_escape_nbsp {
3730 my $value = _html_escape(shift);
3731 $value =~ s/ +/ /g;
3735 #utility methods for print_*
3737 sub _translate_old_latex_format {
3738 warn "_translate_old_latex_format called\n"
3745 if ( $line =~ /^%%Detail\s*$/ ) {
3747 push @template, q![@--!,
3748 q! foreach my $_tr_line (@detail_items) {!,
3749 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3750 q! $_tr_line->{'description'} .= !,
3751 q! "\\tabularnewline\n~~".!,
3752 q! join( "\\tabularnewline\n~~",!,
3753 q! @{$_tr_line->{'ext_description'}}!,
3757 while ( ( my $line_item_line = shift )
3758 !~ /^%%EndDetail\s*$/ ) {
3759 $line_item_line =~ s/'/\\'/g; # nice LTS
3760 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3761 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3762 push @template, " \$OUT .= '$line_item_line';";
3765 push @template, '}',
3768 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3770 push @template, '[@--',
3771 ' foreach my $_tr_line (@total_items) {';
3773 while ( ( my $total_item_line = shift )
3774 !~ /^%%EndTotalDetails\s*$/ ) {
3775 $total_item_line =~ s/'/\\'/g; # nice LTS
3776 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3777 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3778 push @template, " \$OUT .= '$total_item_line';";
3781 push @template, '}',
3785 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3786 push @template, $line;
3792 warn "$_\n" foreach @template;
3800 my $conf = $self->conf;
3802 #check for an invoice-specific override
3803 return $self->invoice_terms if $self->invoice_terms;
3805 #check for a customer- specific override
3806 my $cust_main = $self->cust_main;
3807 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3809 #use configured default
3810 $conf->config('invoice_default_terms') || '';
3816 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3817 $duedate = $self->_date() + ( $1 * 86400 );
3824 $self->due_date ? time2str(shift, $self->due_date) : '';
3827 sub balance_due_msg {
3829 my $msg = $self->mt('Balance Due');
3830 return $msg unless $self->terms;
3831 if ( $self->due_date ) {
3832 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3833 $self->due_date2str($date_format);
3834 } elsif ( $self->terms ) {
3835 $msg .= ' - '. $self->terms;
3840 sub balance_due_date {
3842 my $conf = $self->conf;
3844 if ( $conf->exists('invoice_default_terms')
3845 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3846 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3851 sub credit_balance_msg {
3853 $self->mt('Credit Balance Remaining')
3856 =item invnum_date_pretty
3858 Returns a string with the invoice number and date, for example:
3859 "Invoice #54 (3/20/2008)"
3863 sub invnum_date_pretty {
3865 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3870 Returns a string with the date, for example: "3/20/2008"
3876 time2str($date_format, $self->_date);
3879 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3881 Generate section information for all items appearing on this invoice.
3882 This will only be called for multi-section invoices.
3884 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3885 related display records (L<FS::cust_bill_pkg_display>) and organize
3886 them into two groups ("early" and "late" according to whether they come
3887 before or after the total), then into sections. A subtotal is calculated
3890 Section descriptions are returned in sort weight order. Each consists
3891 of a hash containing:
3893 description: the package category name, escaped
3894 subtotal: the total charges in that section
3895 tax_section: a flag indicating that the section contains only tax charges
3896 summarized: same as tax_section, for some reason
3897 sort_weight: the package category's sort weight
3899 If 'condense' is set on the display record, it also contains everything
3900 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3901 coderefs to generate parts of the invoice. This is not advised.
3905 LATE: an arrayref to push the "late" section hashes onto. The "early"
3906 group is simply returned from the method.
3908 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3909 Turning this on has the following effects:
3910 - Ignores display items with the 'summary' flag.
3911 - Combines all items into the "early" group.
3912 - Creates sections for all non-disabled package categories, even if they
3913 have no charges on this invoice, as well as a section with no name.
3915 ESCAPE: an escape function to use for section titles.
3917 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3918 sorted list. If there are any of these, section subtotals exclude
3921 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3922 passed through to C<_condense_section()>.
3926 use vars qw(%pkg_category_cache);
3927 sub _items_sections {
3930 my $summarypage = shift;
3932 my $extra_sections = shift;
3936 my %late_subtotal = ();
3939 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3942 my $usage = $cust_bill_pkg->usage;
3944 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3945 next if ( $display->summary && $summarypage );
3947 my $section = $display->section;
3948 my $type = $display->type;
3950 $not_tax{$section} = 1
3951 unless $cust_bill_pkg->pkgnum == 0;
3953 if ( $display->post_total && !$summarypage ) {
3954 if (! $type || $type eq 'S') {
3955 $late_subtotal{$section} += $cust_bill_pkg->setup
3956 if $cust_bill_pkg->setup != 0
3957 || $cust_bill_pkg->setup_show_zero;
3961 $late_subtotal{$section} += $cust_bill_pkg->recur
3962 if $cust_bill_pkg->recur != 0
3963 || $cust_bill_pkg->recur_show_zero;
3966 if ($type && $type eq 'R') {
3967 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3968 if $cust_bill_pkg->recur != 0
3969 || $cust_bill_pkg->recur_show_zero;
3972 if ($type && $type eq 'U') {
3973 $late_subtotal{$section} += $usage
3974 unless scalar(@$extra_sections);
3979 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3981 if (! $type || $type eq 'S') {
3982 $subtotal{$section} += $cust_bill_pkg->setup
3983 if $cust_bill_pkg->setup != 0
3984 || $cust_bill_pkg->setup_show_zero;
3988 $subtotal{$section} += $cust_bill_pkg->recur
3989 if $cust_bill_pkg->recur != 0
3990 || $cust_bill_pkg->recur_show_zero;
3993 if ($type && $type eq 'R') {
3994 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3995 if $cust_bill_pkg->recur != 0
3996 || $cust_bill_pkg->recur_show_zero;
3999 if ($type && $type eq 'U') {
4000 $subtotal{$section} += $usage
4001 unless scalar(@$extra_sections);
4010 %pkg_category_cache = ();
4012 push @$late, map { { 'description' => &{$escape}($_),
4013 'subtotal' => $late_subtotal{$_},
4015 'sort_weight' => ( _pkg_category($_)
4016 ? _pkg_category($_)->weight
4019 ((_pkg_category($_) && _pkg_category($_)->condense)
4020 ? $self->_condense_section($format)
4024 sort _sectionsort keys %late_subtotal;
4027 if ( $summarypage ) {
4028 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4029 map { $_->categoryname } qsearch('pkg_category', {});
4030 push @sections, '' if exists($subtotal{''});
4032 @sections = keys %subtotal;
4035 my @early = map { { 'description' => &{$escape}($_),
4036 'subtotal' => $subtotal{$_},
4037 'summarized' => $not_tax{$_} ? '' : 'Y',
4038 'tax_section' => $not_tax{$_} ? '' : 'Y',
4039 'sort_weight' => ( _pkg_category($_)
4040 ? _pkg_category($_)->weight
4043 ((_pkg_category($_) && _pkg_category($_)->condense)
4044 ? $self->_condense_section($format)
4049 push @early, @$extra_sections if $extra_sections;
4051 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4055 #helper subs for above
4058 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4062 my $categoryname = shift;
4063 $pkg_category_cache{$categoryname} ||=
4064 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4067 my %condensed_format = (
4068 'label' => [ qw( Description Qty Amount ) ],
4070 sub { shift->{description} },
4071 sub { shift->{quantity} },
4072 sub { my($href, %opt) = @_;
4073 ($opt{dollar} || ''). $href->{amount};
4076 'align' => [ qw( l r r ) ],
4077 'span' => [ qw( 5 1 1 ) ], # unitprices?
4078 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4081 sub _condense_section {
4082 my ( $self, $format ) = ( shift, shift );
4084 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4085 qw( description_generator
4088 total_line_generator
4093 sub _condensed_generator_defaults {
4094 my ( $self, $format ) = ( shift, shift );
4095 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4104 sub _condensed_header_generator {
4105 my ( $self, $format ) = ( shift, shift );
4107 my ( $f, $prefix, $suffix, $separator, $column ) =
4108 _condensed_generator_defaults($format);
4110 if ($format eq 'latex') {
4111 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4112 $suffix = "\\\\\n\\hline";
4115 sub { my ($d,$a,$s,$w) = @_;
4116 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4118 } elsif ( $format eq 'html' ) {
4119 $prefix = '<th></th>';
4123 sub { my ($d,$a,$s,$w) = @_;
4124 return qq!<th align="$html_align{$a}">$d</th>!;
4132 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4134 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4137 $prefix. join($separator, @result). $suffix;
4142 sub _condensed_description_generator {
4143 my ( $self, $format ) = ( shift, shift );
4145 my ( $f, $prefix, $suffix, $separator, $column ) =
4146 _condensed_generator_defaults($format);
4148 my $money_char = '$';
4149 if ($format eq 'latex') {
4150 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4152 $separator = " & \n";
4154 sub { my ($d,$a,$s,$w) = @_;
4155 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4157 $money_char = '\\dollar';
4158 }elsif ( $format eq 'html' ) {
4159 $prefix = '"><td align="center"></td>';
4163 sub { my ($d,$a,$s,$w) = @_;
4164 return qq!<td align="$html_align{$a}">$d</td>!;
4166 #$money_char = $conf->config('money_char') || '$';
4167 $money_char = ''; # this is madness
4175 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4177 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4179 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4180 map { $f->{$_}->[$i] } qw(align span width)
4184 $prefix. join( $separator, @result ). $suffix;
4189 sub _condensed_total_generator {
4190 my ( $self, $format ) = ( shift, shift );
4192 my ( $f, $prefix, $suffix, $separator, $column ) =
4193 _condensed_generator_defaults($format);
4196 if ($format eq 'latex') {
4199 $separator = " & \n";
4201 sub { my ($d,$a,$s,$w) = @_;
4202 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4204 }elsif ( $format eq 'html' ) {
4208 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4210 sub { my ($d,$a,$s,$w) = @_;
4211 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4220 # my $r = &{$f->{fields}->[$i]}(@args);
4221 # $r .= ' Total' unless $i;
4223 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4225 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4226 map { $f->{$_}->[$i] } qw(align span width)
4230 $prefix. join( $separator, @result ). $suffix;
4235 =item total_line_generator FORMAT
4237 Returns a coderef used for generation of invoice total line items for this
4238 usage_class. FORMAT is either html or latex
4242 # should not be used: will have issues with hash element names (description vs
4243 # total_item and amount vs total_amount -- another array of functions?
4245 sub _condensed_total_line_generator {
4246 my ( $self, $format ) = ( shift, shift );
4248 my ( $f, $prefix, $suffix, $separator, $column ) =
4249 _condensed_generator_defaults($format);
4252 if ($format eq 'latex') {
4255 $separator = " & \n";
4257 sub { my ($d,$a,$s,$w) = @_;
4258 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4260 }elsif ( $format eq 'html' ) {
4264 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4266 sub { my ($d,$a,$s,$w) = @_;
4267 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4276 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4278 &{$column}( &{$f->{fields}->[$i]}(@args),
4279 map { $f->{$_}->[$i] } qw(align span width)
4283 $prefix. join( $separator, @result ). $suffix;
4288 #sub _items_extra_usage_sections {
4290 # my $escape = shift;
4292 # my %sections = ();
4294 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4295 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4297 # next unless $cust_bill_pkg->pkgnum > 0;
4299 # foreach my $section ( keys %usage_class ) {
4301 # my $usage = $cust_bill_pkg->usage($section);
4303 # next unless $usage && $usage > 0;
4305 # $sections{$section} ||= 0;
4306 # $sections{$section} += $usage;
4312 # map { { 'description' => &{$escape}($_),
4313 # 'subtotal' => $sections{$_},
4314 # 'summarized' => '',
4315 # 'tax_section' => '',
4318 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4322 sub _items_extra_usage_sections {
4324 my $conf = $self->conf;
4332 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4334 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4335 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4336 next unless $cust_bill_pkg->pkgnum > 0;
4338 foreach my $classnum ( keys %usage_class ) {
4339 my $section = $usage_class{$classnum}->classname;
4340 $classnums{$section} = $classnum;
4342 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4343 my $amount = $detail->amount;
4344 next unless $amount && $amount > 0;
4346 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4347 $sections{$section}{amount} += $amount; #subtotal
4348 $sections{$section}{calls}++;
4349 $sections{$section}{duration} += $detail->duration;
4351 my $desc = $detail->regionname;
4352 my $description = $desc;
4353 $description = substr($desc, 0, $maxlength). '...'
4354 if $format eq 'latex' && length($desc) > $maxlength;
4356 $lines{$section}{$desc} ||= {
4357 description => &{$escape}($description),
4358 #pkgpart => $part_pkg->pkgpart,
4359 pkgnum => $cust_bill_pkg->pkgnum,
4364 #unit_amount => $cust_bill_pkg->unitrecur,
4365 quantity => $cust_bill_pkg->quantity,
4366 product_code => 'N/A',
4367 ext_description => [],
4370 $lines{$section}{$desc}{amount} += $amount;
4371 $lines{$section}{$desc}{calls}++;
4372 $lines{$section}{$desc}{duration} += $detail->duration;
4378 my %sectionmap = ();
4379 foreach (keys %sections) {
4380 my $usage_class = $usage_class{$classnums{$_}};
4381 $sectionmap{$_} = { 'description' => &{$escape}($_),
4382 'amount' => $sections{$_}{amount}, #subtotal
4383 'calls' => $sections{$_}{calls},
4384 'duration' => $sections{$_}{duration},
4386 'tax_section' => '',
4387 'sort_weight' => $usage_class->weight,
4388 ( $usage_class->format
4389 ? ( map { $_ => $usage_class->$_($format) }
4390 qw( description_generator header_generator total_generator total_line_generator )
4397 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4401 foreach my $section ( keys %lines ) {
4402 foreach my $line ( keys %{$lines{$section}} ) {
4403 my $l = $lines{$section}{$line};
4404 $l->{section} = $sectionmap{$section};
4405 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4406 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4411 return(\@sections, \@lines);
4417 my $end = $self->_date;
4419 # start at date of previous invoice + 1 second or 0 if no previous invoice
4420 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4421 $start = 0 if !$start;
4424 my $cust_main = $self->cust_main;
4425 my @pkgs = $cust_main->all_pkgs;
4426 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4429 foreach my $pkg ( @pkgs ) {
4430 my @h_cust_svc = $pkg->h_cust_svc($end);
4431 foreach my $h_cust_svc ( @h_cust_svc ) {
4432 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4433 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4435 my $inserted = $h_cust_svc->date_inserted;
4436 my $deleted = $h_cust_svc->date_deleted;
4437 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4439 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4441 # DID either activated or ported in; cannot be both for same DID simultaneously
4442 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4443 && (!$phone_inserted->lnp_status
4444 || $phone_inserted->lnp_status eq ''
4445 || $phone_inserted->lnp_status eq 'native')) {
4448 else { # this one not so clean, should probably move to (h_)svc_phone
4449 my $phone_portedin = qsearchs( 'h_svc_phone',
4450 { 'svcnum' => $h_cust_svc->svcnum,
4451 'lnp_status' => 'portedin' },
4452 FS::h_svc_phone->sql_h_searchs($end),
4454 $num_portedin++ if $phone_portedin;
4457 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4458 if($deleted >= $start && $deleted <= $end && $phone_deleted
4459 && (!$phone_deleted->lnp_status
4460 || $phone_deleted->lnp_status ne 'portingout')) {
4463 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4464 && $phone_deleted->lnp_status
4465 && $phone_deleted->lnp_status eq 'portingout') {
4469 # increment usage minutes
4470 if ( $phone_inserted ) {
4471 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4472 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4475 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4478 # don't look at this service again
4479 push @seen, $h_cust_svc->svcnum;
4483 $minutes = sprintf("%d", $minutes);
4484 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4485 . "$num_deactivated Ported-Out: $num_portedout ",
4486 "Total Minutes: $minutes");
4489 sub _items_accountcode_cdr {
4494 my $section = { 'amount' => 0,
4497 'sort_weight' => '',
4499 'description' => 'Usage by Account Code',
4505 my %accountcodes = ();
4507 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4508 next unless $cust_bill_pkg->pkgnum > 0;
4510 my @header = $cust_bill_pkg->details_header;
4511 next unless scalar(@header);
4512 $section->{'header'} = join(',',@header);
4514 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4516 $section->{'header'} = $detail->formatted('format' => $format)
4517 if($detail->detail eq $section->{'header'});
4519 my $accountcode = $detail->accountcode;
4520 next unless $accountcode;
4522 my $amount = $detail->amount;
4523 next unless $amount && $amount > 0;
4525 $accountcodes{$accountcode} ||= {
4526 description => $accountcode,
4533 product_code => 'N/A',
4534 section => $section,
4535 ext_description => [ $section->{'header'} ],
4539 $section->{'amount'} += $amount;
4540 $accountcodes{$accountcode}{'amount'} += $amount;
4541 $accountcodes{$accountcode}{calls}++;
4542 $accountcodes{$accountcode}{duration} += $detail->duration;
4543 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4547 foreach my $l ( values %accountcodes ) {
4548 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4549 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4550 foreach my $sorted_detail ( @sorted_detail ) {
4551 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4553 delete $l->{detail_temp};
4557 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4559 return ($section,\@sorted_lines);
4562 sub _items_svc_phone_sections {
4564 my $conf = $self->conf;
4572 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4574 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4575 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4577 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4578 next unless $cust_bill_pkg->pkgnum > 0;
4580 my @header = $cust_bill_pkg->details_header;
4581 next unless scalar(@header);
4583 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4585 my $phonenum = $detail->phonenum;
4586 next unless $phonenum;
4588 my $amount = $detail->amount;
4589 next unless $amount && $amount > 0;
4591 $sections{$phonenum} ||= { 'amount' => 0,
4594 'sort_weight' => -1,
4595 'phonenum' => $phonenum,
4597 $sections{$phonenum}{amount} += $amount; #subtotal
4598 $sections{$phonenum}{calls}++;
4599 $sections{$phonenum}{duration} += $detail->duration;
4601 my $desc = $detail->regionname;
4602 my $description = $desc;
4603 $description = substr($desc, 0, $maxlength). '...'
4604 if $format eq 'latex' && length($desc) > $maxlength;
4606 $lines{$phonenum}{$desc} ||= {
4607 description => &{$escape}($description),
4608 #pkgpart => $part_pkg->pkgpart,
4616 product_code => 'N/A',
4617 ext_description => [],
4620 $lines{$phonenum}{$desc}{amount} += $amount;
4621 $lines{$phonenum}{$desc}{calls}++;
4622 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4624 my $line = $usage_class{$detail->classnum}->classname;
4625 $sections{"$phonenum $line"} ||=
4629 'sort_weight' => $usage_class{$detail->classnum}->weight,
4630 'phonenum' => $phonenum,
4631 'header' => [ @header ],
4633 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4634 $sections{"$phonenum $line"}{calls}++;
4635 $sections{"$phonenum $line"}{duration} += $detail->duration;
4637 $lines{"$phonenum $line"}{$desc} ||= {
4638 description => &{$escape}($description),
4639 #pkgpart => $part_pkg->pkgpart,
4647 product_code => 'N/A',
4648 ext_description => [],
4651 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4652 $lines{"$phonenum $line"}{$desc}{calls}++;
4653 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4654 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4655 $detail->formatted('format' => $format);
4660 my %sectionmap = ();
4661 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4662 foreach ( keys %sections ) {
4663 my @header = @{ $sections{$_}{header} || [] };
4665 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4666 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4667 my $usage_class = $summary ? $simple : $usage_simple;
4668 my $ending = $summary ? ' usage charges' : '';
4671 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4673 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4674 'amount' => $sections{$_}{amount}, #subtotal
4675 'calls' => $sections{$_}{calls},
4676 'duration' => $sections{$_}{duration},
4678 'tax_section' => '',
4679 'phonenum' => $sections{$_}{phonenum},
4680 'sort_weight' => $sections{$_}{sort_weight},
4681 'post_total' => $summary, #inspire pagebreak
4683 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4684 qw( description_generator
4687 total_line_generator
4694 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4695 $a->{sort_weight} <=> $b->{sort_weight}
4700 foreach my $section ( keys %lines ) {
4701 foreach my $line ( keys %{$lines{$section}} ) {
4702 my $l = $lines{$section}{$line};
4703 $l->{section} = $sectionmap{$section};
4704 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4705 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4710 if($conf->exists('phone_usage_class_summary')) {
4711 # this only works with Latex
4715 # after this, we'll have only two sections per DID:
4716 # Calls Summary and Calls Detail
4717 foreach my $section ( @sections ) {
4718 if($section->{'post_total'}) {
4719 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4720 $section->{'total_line_generator'} = sub { '' };
4721 $section->{'total_generator'} = sub { '' };
4722 $section->{'header_generator'} = sub { '' };
4723 $section->{'description_generator'} = '';
4724 push @newsections, $section;
4725 my %calls_detail = %$section;
4726 $calls_detail{'post_total'} = '';
4727 $calls_detail{'sort_weight'} = '';
4728 $calls_detail{'description_generator'} = sub { '' };
4729 $calls_detail{'header_generator'} = sub {
4730 return ' & Date/Time & Called Number & Duration & Price'
4731 if $format eq 'latex';
4734 $calls_detail{'description'} = 'Calls Detail: '
4735 . $section->{'phonenum'};
4736 push @newsections, \%calls_detail;
4740 # after this, each usage class is collapsed/summarized into a single
4741 # line under the Calls Summary section
4742 foreach my $newsection ( @newsections ) {
4743 if($newsection->{'post_total'}) { # this means Calls Summary
4744 foreach my $section ( @sections ) {
4745 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4746 && !$section->{'post_total'});
4747 my $newdesc = $section->{'description'};
4748 my $tn = $section->{'phonenum'};
4749 $newdesc =~ s/$tn//g;
4750 my $line = { ext_description => [],
4754 calls => $section->{'calls'},
4755 section => $newsection,
4756 duration => $section->{'duration'},
4757 description => $newdesc,
4758 amount => sprintf("%.2f",$section->{'amount'}),
4759 product_code => 'N/A',
4761 push @newlines, $line;
4766 # after this, Calls Details is populated with all CDRs
4767 foreach my $newsection ( @newsections ) {
4768 if(!$newsection->{'post_total'}) { # this means Calls Details
4769 foreach my $line ( @lines ) {
4770 next unless (scalar(@{$line->{'ext_description'}}) &&
4771 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4773 my @extdesc = @{$line->{'ext_description'}};
4775 foreach my $extdesc ( @extdesc ) {
4776 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4777 push @newextdesc, $extdesc;
4779 $line->{'ext_description'} = \@newextdesc;
4780 $line->{'section'} = $newsection;
4781 push @newlines, $line;
4786 return(\@newsections, \@newlines);
4789 return(\@sections, \@lines);
4793 sub _items { # seems to be unused
4796 #my @display = scalar(@_)
4798 # : qw( _items_previous _items_pkg );
4799 # #: qw( _items_pkg );
4800 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4801 my @display = qw( _items_previous _items_pkg );
4804 foreach my $display ( @display ) {
4805 push @b, $self->$display(@_);
4810 sub _items_previous {
4812 my $conf = $self->conf;
4813 my $cust_main = $self->cust_main;
4814 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4816 foreach ( @pr_cust_bill ) {
4817 my $date = $conf->exists('invoice_show_prior_due_date')
4818 ? 'due '. $_->due_date2str($date_format)
4819 : time2str($date_format, $_->_date);
4821 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4822 #'pkgpart' => 'N/A',
4824 'amount' => sprintf("%.2f", $_->owed),
4830 # 'description' => 'Previous Balance',
4831 # #'pkgpart' => 'N/A',
4832 # 'pkgnum' => 'N/A',
4833 # 'amount' => sprintf("%10.2f", $pr_total ),
4834 # 'ext_description' => [ map {
4835 # "Invoice ". $_->invnum.
4836 # " (". time2str("%x",$_->_date). ") ".
4837 # sprintf("%10.2f", $_->owed)
4838 # } @pr_cust_bill ],
4843 =item _items_pkg [ OPTIONS ]
4845 Return line item hashes for each package item on this invoice. Nearly
4848 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4850 The only OPTIONS accepted is 'section', which may point to a hashref
4851 with a key named 'condensed', which may have a true value. If it
4852 does, this method tries to merge identical items into items with
4853 'quantity' equal to the number of items (not the sum of their
4854 separate quantities, for some reason).
4862 warn "$me _items_pkg searching for all package line items\n"
4865 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4867 warn "$me _items_pkg filtering line items\n"
4869 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4871 if ($options{section} && $options{section}->{condensed}) {
4873 warn "$me _items_pkg condensing section\n"
4877 local $Storable::canonical = 1;
4878 foreach ( @items ) {
4880 delete $item->{ref};
4881 delete $item->{ext_description};
4882 my $key = freeze($item);
4883 $itemshash{$key} ||= 0;
4884 $itemshash{$key} ++; # += $item->{quantity};
4886 @items = sort { $a->{description} cmp $b->{description} }
4887 map { my $i = thaw($_);
4888 $i->{quantity} = $itemshash{$_};
4890 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4896 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4903 return 0 unless $a->itemdesc cmp $b->itemdesc;
4904 return -1 if $b->itemdesc eq 'Tax';
4905 return 1 if $a->itemdesc eq 'Tax';
4906 return -1 if $b->itemdesc eq 'Other surcharges';
4907 return 1 if $a->itemdesc eq 'Other surcharges';
4908 $a->itemdesc cmp $b->itemdesc;
4913 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4914 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4917 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4919 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4920 list of hashrefs describing the line items they generate on the invoice.
4922 OPTIONS may include:
4924 format: the invoice format.
4926 escape_function: the function used to escape strings.
4928 DEPRECATED? (expensive, mostly unused?)
4929 format_function: the function used to format CDRs.
4931 section: a hashref containing 'description'; if this is present,
4932 cust_bill_pkg_display records not belonging to this section are
4935 multisection: a flag indicating that this is a multisection invoice,
4936 which does something complicated.
4938 multilocation: a flag to display the location label for the package.
4940 Returns a list of hashrefs, each of which may contain:
4942 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4943 ext_description, which is an arrayref of detail lines to show below
4948 sub _items_cust_bill_pkg {
4950 my $conf = $self->conf;
4951 my $cust_bill_pkgs = shift;
4954 my $format = $opt{format} || '';
4955 my $escape_function = $opt{escape_function} || sub { shift };
4956 my $format_function = $opt{format_function} || '';
4957 my $no_usage = $opt{no_usage} || '';
4958 my $unsquelched = $opt{unsquelched} || ''; #unused
4959 my $section = $opt{section}->{description} if $opt{section};
4960 my $summary_page = $opt{summary_page} || ''; #unused
4961 my $multilocation = $opt{multilocation} || '';
4962 my $multisection = $opt{multisection} || '';
4963 my $discount_show_always = 0;
4965 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4967 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4970 my ($s, $r, $u) = ( undef, undef, undef );
4971 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4974 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4975 if ( $_ && !$cust_bill_pkg->hidden ) {
4976 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4977 $_->{amount} =~ s/^\-0\.00$/0.00/;
4978 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4980 if $_->{amount} != 0
4981 || $discount_show_always
4982 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4983 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4989 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4991 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4992 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4995 foreach my $display ( grep { defined($section)
4996 ? $_->section eq $section
4999 #grep { !$_->summary || !$summary_page } # bunk!
5000 grep { !$_->summary || $multisection }
5001 @cust_bill_pkg_display
5005 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5006 $display->billpkgdisplaynum. "\n"
5009 my $type = $display->type;
5011 my $desc = $cust_bill_pkg->desc;
5012 $desc = substr($desc, 0, $maxlength). '...'
5013 if $format eq 'latex' && length($desc) > $maxlength;
5015 my %details_opt = ( 'format' => $format,
5016 'escape_function' => $escape_function,
5017 'format_function' => $format_function,
5018 'no_usage' => $opt{'no_usage'},
5021 if ( $cust_bill_pkg->pkgnum > 0 ) {
5023 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5026 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5028 # which pkgpart to show for display purposes?
5029 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5031 # start/end dates for invoice formats that do nonstandard
5033 my %item_dates = ();
5034 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5035 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5037 if ( (!$type || $type eq 'S')
5038 && ( $cust_bill_pkg->setup != 0
5039 || $cust_bill_pkg->setup_show_zero
5044 warn "$me _items_cust_bill_pkg adding setup\n"
5047 my $description = $desc;
5048 $description .= ' Setup'
5049 if $cust_bill_pkg->recur != 0
5050 || $discount_show_always
5051 || $cust_bill_pkg->recur_show_zero;
5055 unless ( $cust_pkg->part_pkg->hide_svc_detail
5056 || $cust_bill_pkg->hidden )
5059 my @svc_labels = map &{$escape_function}($_),
5060 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5061 push @d, @svc_labels
5062 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5063 $svc_label = $svc_labels[0];
5065 if ( $multilocation ) {
5066 my $loc = $cust_pkg->location_label;
5067 $loc = substr($loc, 0, $maxlength). '...'
5068 if $format eq 'latex' && length($loc) > $maxlength;
5069 push @d, &{$escape_function}($loc);
5072 } #unless hiding service details
5074 push @d, $cust_bill_pkg->details(%details_opt)
5075 if $cust_bill_pkg->recur == 0;
5077 if ( $cust_bill_pkg->hidden ) {
5078 $s->{amount} += $cust_bill_pkg->setup;
5079 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5080 push @{ $s->{ext_description} }, @d;
5084 description => $description,
5085 pkgpart => $pkgpart,
5086 pkgnum => $cust_bill_pkg->pkgnum,
5087 amount => $cust_bill_pkg->setup,
5088 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5089 unit_amount => $cust_bill_pkg->unitsetup,
5090 quantity => $cust_bill_pkg->quantity,
5091 ext_description => \@d,
5092 svc_label => ($svc_label || ''),
5098 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5100 $cust_bill_pkg->recur != 0
5101 || $cust_bill_pkg->setup == 0
5102 || $discount_show_always
5103 || $cust_bill_pkg->recur_show_zero
5108 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5111 my $is_summary = $display->summary;
5112 my $description = ($is_summary && $type && $type eq 'U')
5113 ? "Usage charges" : $desc;
5115 my $part_pkg = $cust_pkg->part_pkg;
5117 #pry be a bit more efficient to look some of this conf stuff up
5120 $conf->exists('disable_line_item_date_ranges')
5121 || $part_pkg->option('disable_line_item_date_ranges',1)
5122 || ! $cust_bill_pkg->sdate
5123 || ! $cust_bill_pkg->edate
5126 my $date_style = '';
5127 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5128 $cust_main->agentnum
5130 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5131 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5132 $cust_main->agentnum
5134 if ( defined($date_style) && $date_style eq 'month_of' ) {
5135 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5136 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5137 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5138 $cust_main->agentnum
5140 $desc .= ' ' unless $desc =~ /\s$/;
5141 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5143 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5144 " - ". time2str($date_format, $cust_bill_pkg->edate);
5146 $description .= " ($time_period)";
5150 my @seconds = (); # for display of usage info
5153 #at least until cust_bill_pkg has "past" ranges in addition to
5154 #the "future" sdate/edate ones... see #3032
5155 my @dates = ( $self->_date );
5156 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5157 push @dates, $prev->sdate if $prev;
5158 push @dates, undef if !$prev;
5160 unless ( $cust_pkg->part_pkg->hide_svc_detail
5161 || $cust_bill_pkg->itemdesc
5162 || $cust_bill_pkg->hidden
5163 || $is_summary && $type && $type eq 'U' )
5166 warn "$me _items_cust_bill_pkg adding service details\n"
5169 my @svc_labels = map &{$escape_function}($_),
5170 $cust_pkg->h_labels_short(@dates, 'I');
5171 push @d, @svc_labels
5172 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5173 $svc_label = $svc_labels[0];
5175 warn "$me _items_cust_bill_pkg done adding service details\n"
5178 if ( $multilocation ) {
5179 my $loc = $cust_pkg->location_label;
5180 $loc = substr($loc, 0, $maxlength). '...'
5181 if $format eq 'latex' && length($loc) > $maxlength;
5182 push @d, &{$escape_function}($loc);
5185 # Display of seconds_since_sqlradacct:
5186 # On the invoice, when processing @detail_items, look for a field
5187 # named 'seconds'. This will contain total seconds for each
5188 # service, in the same order as @ext_description. For services
5189 # that don't support this it will show undef.
5190 if ( $conf->exists('svc_acct-usage_seconds')
5191 and ! $cust_bill_pkg->pkgpart_override ) {
5192 foreach my $cust_svc (
5193 $cust_pkg->h_cust_svc(@dates, 'I')
5196 # eval because not having any part_export_usage exports
5197 # is a fatal error, last_bill/_date because that's how
5198 # sqlradius_hour billing does it
5200 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5202 push @seconds, $sec;
5204 } #if svc_acct-usage_seconds
5208 unless ( $is_summary ) {
5209 warn "$me _items_cust_bill_pkg adding details\n"
5212 #instead of omitting details entirely in this case (unwanted side
5213 # effects), just omit CDRs
5214 $details_opt{'no_usage'} = 1
5215 if $type && $type eq 'R';
5217 push @d, $cust_bill_pkg->details(%details_opt);
5220 warn "$me _items_cust_bill_pkg calculating amount\n"
5225 $amount = $cust_bill_pkg->recur;
5226 } elsif ($type eq 'R') {
5227 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5228 } elsif ($type eq 'U') {
5229 $amount = $cust_bill_pkg->usage;
5232 if ( !$type || $type eq 'R' ) {
5234 warn "$me _items_cust_bill_pkg adding recur\n"
5237 if ( $cust_bill_pkg->hidden ) {
5238 $r->{amount} += $amount;
5239 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5240 push @{ $r->{ext_description} }, @d;
5243 description => $description,
5244 pkgpart => $pkgpart,
5245 pkgnum => $cust_bill_pkg->pkgnum,
5247 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5248 unit_amount => $cust_bill_pkg->unitrecur,
5249 quantity => $cust_bill_pkg->quantity,
5251 ext_description => \@d,
5252 svc_label => ($svc_label || ''),
5254 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5257 } else { # $type eq 'U'
5259 warn "$me _items_cust_bill_pkg adding usage\n"
5262 if ( $cust_bill_pkg->hidden ) {
5263 $u->{amount} += $amount;
5264 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5265 push @{ $u->{ext_description} }, @d;
5268 description => $description,
5269 pkgpart => $pkgpart,
5270 pkgnum => $cust_bill_pkg->pkgnum,
5272 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5273 unit_amount => $cust_bill_pkg->unitrecur,
5274 quantity => $cust_bill_pkg->quantity,
5276 ext_description => \@d,
5281 } # recurring or usage with recurring charge
5283 } else { #pkgnum tax or one-shot line item (??)
5285 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5288 if ( $cust_bill_pkg->setup != 0 ) {
5290 'description' => $desc,
5291 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5294 if ( $cust_bill_pkg->recur != 0 ) {
5296 'description' => "$desc (".
5297 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5298 time2str($date_format, $cust_bill_pkg->edate). ')',
5299 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5307 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5308 && $conf->exists('discount-show-always'));
5312 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5314 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5315 $_->{amount} =~ s/^\-0\.00$/0.00/;
5316 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5318 if $_->{amount} != 0
5319 || $discount_show_always
5320 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5321 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5325 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5332 sub _items_credits {
5333 my( $self, %opt ) = @_;
5334 my $trim_len = $opt{'trim_len'} || 60;
5339 if ( $self->conf->exists('previous_balance-payments_since') ) {
5341 $date = $self->previous_bill->_date if $self->previous_bill;
5342 @objects = qsearch('cust_credit', {
5343 'custnum' => $self->custnum,
5344 '_date' => {op => '>=', value => $date},
5346 # hard to do this in the qsearch...
5347 @objects = grep { $_->_date < $self->_date } @objects;
5349 @objects = $self->cust_credited;
5352 foreach my $obj ( @objects ) {
5353 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5355 my $reason = substr($cust_credit->reason, 0, $trim_len);
5356 $reason .= '...' if length($reason) < length($cust_credit->reason);
5357 $reason = " ($reason) " if $reason;
5360 #'description' => 'Credit ref\#'. $_->crednum.
5361 # " (". time2str("%x",$_->cust_credit->_date) .")".
5363 'description' => $self->mt('Credit applied').' '.
5364 time2str($date_format,$obj->_date). $reason,
5365 'amount' => sprintf("%.2f",$obj->amount),
5373 sub _items_payments {
5377 my $detailed = $self->conf->exists('invoice_payment_details');
5379 if ( $self->conf->exists('previous_balance-payments_since') ) {
5381 $date = $self->previous_bill->_date if $self->previous_bill;
5382 @objects = qsearch('cust_pay', {
5383 'custnum' => $self->custnum,
5384 '_date' => {op => '>=', value => $date},
5386 @objects = grep { $_->_date < $self->_date } @objects;
5388 @objects = $self->cust_bill_pay;
5391 foreach my $obj (@objects) {
5392 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5393 my $desc = $self->mt('Payment received').' '.
5394 time2str($date_format, $cust_pay->_date );
5395 $desc .= $self->mt(' via ' . $cust_pay->payby_payinfo_pretty)
5399 'description' => $desc,
5400 'amount' => sprintf("%.2f", $obj->amount )
5408 =item _items_discounts_avail
5410 Returns an array of line item hashrefs representing available term discounts
5411 for this invoice. This makes the same assumptions that apply to term
5412 discounts in general: that the package is billed monthly, at a flat rate,
5413 with no usage charges. A prorated first month will be handled, as will
5414 a setup fee if the discount is allowed to apply to setup fees.
5418 sub _items_discounts_avail {
5420 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5422 my %plans = $self->discount_plans;
5424 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5428 my $plan = $plans{$months};
5430 my $term_total = sprintf('%.2f', $plan->discounted_total);
5431 my $percent = sprintf('%.0f',
5432 100 * (1 - $term_total / $plan->base_total) );
5433 my $permonth = sprintf('%.2f', $term_total / $months);
5434 my $detail = $self->mt('discount on item'). ' '.
5435 join(', ', map { "#$_" } $plan->pkgnums)
5438 # discounts for non-integer months don't work anyway
5439 $months = sprintf("%d", $months);
5442 description => $self->mt('Save [_1]% by paying for [_2] months',
5444 amount => $self->mt('[_1] ([_2] per month)',
5445 $term_total, $money_char.$permonth),
5446 ext_description => ($detail || ''),
5449 sort { $b <=> $a } keys %plans;
5453 =item call_details [ OPTION => VALUE ... ]
5455 Returns an array of CSV strings representing the call details for this invoice
5456 The only option available is the boolean prepend_billed_number
5461 my ($self, %opt) = @_;
5463 my $format_function = sub { shift };
5465 if ($opt{prepend_billed_number}) {
5466 $format_function = sub {
5470 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5475 my @details = map { $_->details( 'format_function' => $format_function,
5476 'escape_function' => sub{ return() },
5480 $self->cust_bill_pkg;
5481 my $header = $details[0];
5482 ( $header, grep { $_ ne $header } @details );
5492 =item process_reprint
5496 sub process_reprint {
5497 process_re_X('print', @_);
5500 =item process_reemail
5504 sub process_reemail {
5505 process_re_X('email', @_);
5513 process_re_X('fax', @_);
5521 process_re_X('ftp', @_);
5528 sub process_respool {
5529 process_re_X('spool', @_);
5532 use Storable qw(thaw);
5536 my( $method, $job ) = ( shift, shift );
5537 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5539 my $param = thaw(decode_base64(shift));
5540 warn Dumper($param) if $DEBUG;
5551 my($method, $job, %param ) = @_;
5553 warn "re_X $method for job $job with param:\n".
5554 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5557 #some false laziness w/search/cust_bill.html
5559 my $orderby = 'ORDER BY cust_bill._date';
5561 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5563 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5565 my @cust_bill = qsearch( {
5566 #'select' => "cust_bill.*",
5567 'table' => 'cust_bill',
5568 'addl_from' => $addl_from,
5570 'extra_sql' => $extra_sql,
5571 'order_by' => $orderby,
5575 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5577 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5580 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5581 foreach my $cust_bill ( @cust_bill ) {
5582 $cust_bill->$method();
5584 if ( $job ) { #progressbar foo
5586 if ( time - $min_sec > $last ) {
5587 my $error = $job->update_statustext(
5588 int( 100 * $num / scalar(@cust_bill) )
5590 die $error if $error;
5601 =head1 CLASS METHODS
5607 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5612 my ($class, $start, $end) = @_;
5614 $class->paid_sql($start, $end). ' - '.
5615 $class->credited_sql($start, $end);
5620 Returns an SQL fragment to retreive the net amount (charged minus credited).
5625 my ($class, $start, $end) = @_;
5626 'charged - '. $class->credited_sql($start, $end);
5631 Returns an SQL fragment to retreive the amount paid against this invoice.
5636 my ($class, $start, $end) = @_;
5637 $start &&= "AND cust_bill_pay._date <= $start";
5638 $end &&= "AND cust_bill_pay._date > $end";
5639 $start = '' unless defined($start);
5640 $end = '' unless defined($end);
5641 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5642 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5647 Returns an SQL fragment to retreive the amount credited against this invoice.
5652 my ($class, $start, $end) = @_;
5653 $start &&= "AND cust_credit_bill._date <= $start";
5654 $end &&= "AND cust_credit_bill._date > $end";
5655 $start = '' unless defined($start);
5656 $end = '' unless defined($end);
5657 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5658 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5663 Returns an SQL fragment to retrieve the due date of an invoice.
5664 Currently only supported on PostgreSQL.
5669 my $conf = new FS::Conf;
5673 cust_bill.invoice_terms,
5674 cust_main.invoice_terms,
5675 \''.($conf->config('invoice_default_terms') || '').'\'
5676 ), E\'Net (\\\\d+)\'
5678 ) * 86400 + cust_bill._date'
5681 =item search_sql_where HASHREF
5683 Class method which returns an SQL WHERE fragment to search for parameters
5684 specified in HASHREF. Valid parameters are
5690 List reference of start date, end date, as UNIX timestamps.
5700 List reference of charged limits (exclusive).
5704 List reference of charged limits (exclusive).
5708 flag, return open invoices only
5712 flag, return net invoices only
5716 =item newest_percust
5720 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5724 sub search_sql_where {
5725 my($class, $param) = @_;
5727 warn "$me search_sql_where called with params: \n".
5728 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5734 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5735 push @search, "cust_main.agentnum = $1";
5739 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5740 push @search, "cust_main.refnum = $1";
5744 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5745 push @search, "cust_bill.custnum = $1";
5749 if ( $param->{'cust_classnum'} ) {
5750 my $classnums = $param->{'cust_classnum'};
5751 $classnums = [ $classnums ] if !ref($classnums);
5752 $classnums = [ grep /^\d+$/, @$classnums ];
5753 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5758 if ( $param->{_date} ) {
5759 my($beginning, $ending) = @{$param->{_date}};
5761 push @search, "cust_bill._date >= $beginning",
5762 "cust_bill._date < $ending";
5766 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5767 push @search, "cust_bill.invnum >= $1";
5769 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5770 push @search, "cust_bill.invnum <= $1";
5774 if ( $param->{charged} ) {
5775 my @charged = ref($param->{charged})
5776 ? @{ $param->{charged} }
5777 : ($param->{charged});
5779 push @search, map { s/^charged/cust_bill.charged/; $_; }
5783 my $owed_sql = FS::cust_bill->owed_sql;
5786 if ( $param->{owed} ) {
5787 my @owed = ref($param->{owed})
5788 ? @{ $param->{owed} }
5790 push @search, map { s/^owed/$owed_sql/; $_; }
5795 push @search, "0 != $owed_sql"
5796 if $param->{'open'};
5797 push @search, '0 != '. FS::cust_bill->net_sql
5801 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5802 if $param->{'days'};
5805 if ( $param->{'newest_percust'} ) {
5807 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5808 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5810 my @newest_where = map { my $x = $_;
5811 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5814 grep ! /^cust_main./, @search;
5815 my $newest_where = scalar(@newest_where)
5816 ? ' AND '. join(' AND ', @newest_where)
5820 push @search, "cust_bill._date = (
5821 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5822 WHERE newest_cust_bill.custnum = cust_bill.custnum
5828 #promised_date - also has an option to accept nulls
5829 if ( $param->{promised_date} ) {
5830 my($beginning, $ending, $null) = @{$param->{promised_date}};
5832 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5833 "cust_bill.promised_date < $ending )" .
5834 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5837 #agent virtualization
5838 my $curuser = $FS::CurrentUser::CurrentUser;
5839 if ( $curuser->username eq 'fs_queue'
5840 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5842 my $newuser = qsearchs('access_user', {
5843 'username' => $username,
5847 $curuser = $newuser;
5849 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5852 push @search, $curuser->agentnums_sql;
5854 join(' AND ', @search );
5866 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5867 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base