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 # don't localize dates here, they're a defined format
1749 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1750 my $file = "$spooldir/$tracctnum.csv";
1752 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1754 open(CSV, ">$file") or die "can't open $file: $!";
1762 if ( $opt{protocol} eq 'ftp' ) {
1763 eval "use Net::FTP;";
1765 $net = Net::FTP->new($opt{server}) or die @$;
1767 die "unknown protocol: $opt{protocol}";
1770 $net->login( $opt{username}, $opt{password} )
1771 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1773 $net->binary or die "can't set binary mode";
1775 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1777 $net->put($file) or die "can't put $file: $!";
1787 Spools CSV invoice data.
1793 =item format - 'default' or 'billco'
1795 =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>).
1797 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1799 =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.
1806 my($self, %opt) = @_;
1808 my $cust_main = $self->cust_main;
1810 if ( $opt{'dest'} ) {
1811 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1812 $cust_main->invoicing_list;
1813 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1814 || ! keys %invoicing_list;
1817 if ( $opt{'balanceover'} ) {
1819 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1822 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1823 mkdir $spooldir, 0700 unless -d $spooldir;
1825 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1829 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1830 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1833 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1835 open(CSV, ">>$file") or die "can't open $file: $!";
1836 flock(CSV, LOCK_EX);
1841 if ( lc($opt{'format'}) eq 'billco' ) {
1843 flock(CSV, LOCK_UN);
1848 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1851 open(CSV,">>$file") or die "can't open $file: $!";
1852 flock(CSV, LOCK_EX);
1858 flock(CSV, LOCK_UN);
1865 =item print_csv OPTION => VALUE, ...
1867 Returns CSV data for this invoice.
1871 format - 'default' or 'billco'
1873 Returns a list consisting of two scalars. The first is a single line of CSV
1874 header information for this invoice. The second is one or more lines of CSV
1875 detail information for this invoice.
1877 If I<format> is not specified or "default", the fields of the CSV file are as
1880 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1884 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1886 B<record_type> is C<cust_bill> for the initial header line only. The
1887 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1888 fields are filled in.
1890 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1891 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1894 =item invnum - invoice number
1896 =item custnum - customer number
1898 =item _date - invoice date
1900 =item charged - total invoice amount
1902 =item first - customer first name
1904 =item last - customer first name
1906 =item company - company name
1908 =item address1 - address line 1
1910 =item address2 - address line 1
1920 =item pkg - line item description
1922 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1924 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1926 =item sdate - start date for recurring fee
1928 =item edate - end date for recurring fee
1932 If I<format> is "billco", the fields of the header CSV file are as follows:
1934 +-------------------------------------------------------------------+
1935 | FORMAT HEADER FILE |
1936 |-------------------------------------------------------------------|
1937 | Field | Description | Name | Type | Width |
1938 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1939 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1940 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1941 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1942 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1943 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1944 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1945 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1946 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1947 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1948 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1949 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1950 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1951 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1952 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1953 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1954 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1955 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1956 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1957 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1958 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1959 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1960 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1961 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1962 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1963 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1964 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1965 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1966 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1967 +-------+-------------------------------+------------+------+-------+
1969 If I<format> is "billco", the fields of the detail CSV file are as follows:
1971 FORMAT FOR DETAIL FILE
1973 Field | Description | Name | Type | Width
1974 1 | N/A-Leave Empty | RC | CHAR | 2
1975 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1976 3 | Account Number | TRACCTNUM | CHAR | 15
1977 4 | Invoice Number | TRINVOICE | CHAR | 15
1978 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1979 6 | Transaction Detail | DETAILS | CHAR | 100
1980 7 | Amount | AMT | NUM* | 9
1981 8 | Line Format Control** | LNCTRL | CHAR | 2
1982 9 | Grouping Code | GROUP | CHAR | 2
1983 10 | User Defined | ACCT CODE | CHAR | 15
1988 my($self, %opt) = @_;
1990 eval "use Text::CSV_XS";
1993 my $cust_main = $self->cust_main;
1995 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1997 if ( lc($opt{'format'}) eq 'billco' ) {
2000 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2002 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2004 my( $previous_balance, @unused ) = $self->previous; #previous balance
2006 my $pmt_cr_applied = 0;
2007 $pmt_cr_applied += $_->{'amount'}
2008 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
2010 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2013 '', # 1 | N/A-Leave Empty CHAR 2
2014 '', # 2 | N/A-Leave Empty CHAR 15
2015 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
2016 $self->invnum, # 4 | Transaction Invoice No CHAR 15
2017 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
2018 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
2019 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2020 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2021 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2022 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2023 '', # 10 | Ancillary Billing Information CHAR 30
2024 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2025 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2028 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2031 $duedate, # 14 | Bill Due Date CHAR 10
2033 $previous_balance, # 15 | Previous Balance NUM* 9
2034 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2035 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2036 $totaldue, # 18 | Total Amt Due NUM* 9
2037 $totaldue, # 19 | Total Amt Due NUM* 9
2038 '', # 20 | 30 Day Aging NUM* 9
2039 '', # 21 | 60 Day Aging NUM* 9
2040 '', # 22 | 90 Day Aging NUM* 9
2041 'N', # 23 | Y/N CHAR 1
2042 '', # 24 | Remittance automation CHAR 100
2043 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2044 $self->custnum, # 26 | Customer Reference Number CHAR 15
2045 '0', # 27 | Federal Tax*** NUM* 9
2046 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2047 '0', # 29 | Other Taxes & Fees*** NUM* 9
2050 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2052 my ($previous_balance) = $self->previous;
2053 $previous_balance = sprintf('%.2f', $previous_balance);
2054 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2060 $self->_items_pkg, #_items_nontax? no sections or anything
2065 $cust_main->agentnum,
2066 $cust_main->agent->agent,
2070 $cust_main->company,
2071 $cust_main->address1,
2072 $cust_main->address2,
2078 time2str("%x", $self->_date),
2083 $self->due_date2str("%x"),
2094 time2str("%x", $self->_date),
2095 sprintf("%.2f", $self->charged),
2096 ( map { $cust_main->getfield($_) }
2097 qw( first last company address1 address2 city state zip country ) ),
2099 ) or die "can't create csv";
2102 my $header = $csv->string. "\n";
2105 if ( lc($opt{'format'}) eq 'billco' ) {
2108 foreach my $item ( $self->_items_pkg ) {
2111 '', # 1 | N/A-Leave Empty CHAR 2
2112 '', # 2 | N/A-Leave Empty CHAR 15
2113 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2114 $self->invnum, # 4 | Invoice Number CHAR 15
2115 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2116 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2117 $item->{'amount'}, # 7 | Amount NUM* 9
2118 '', # 8 | Line Format Control** CHAR 2
2119 '', # 9 | Grouping Code CHAR 2
2120 '', # 10 | User Defined CHAR 15
2123 $detail .= $csv->string. "\n";
2127 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2133 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2135 my($pkg, $setup, $recur, $sdate, $edate);
2136 if ( $cust_bill_pkg->pkgnum ) {
2138 ($pkg, $setup, $recur, $sdate, $edate) = (
2139 $cust_bill_pkg->part_pkg->pkg,
2140 ( $cust_bill_pkg->setup != 0
2141 ? sprintf("%.2f", $cust_bill_pkg->setup )
2143 ( $cust_bill_pkg->recur != 0
2144 ? sprintf("%.2f", $cust_bill_pkg->recur )
2146 ( $cust_bill_pkg->sdate
2147 ? time2str("%x", $cust_bill_pkg->sdate)
2149 ($cust_bill_pkg->edate
2150 ? time2str("%x", $cust_bill_pkg->edate)
2154 } else { #pkgnum tax
2155 next unless $cust_bill_pkg->setup != 0;
2156 $pkg = $cust_bill_pkg->desc;
2157 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2158 ( $sdate, $edate ) = ( '', '' );
2164 ( map { '' } (1..11) ),
2165 ($pkg, $setup, $recur, $sdate, $edate)
2166 ) or die "can't create csv";
2168 $detail .= $csv->string. "\n";
2174 ( $header, $detail );
2180 Pays this invoice with a compliemntary payment. If there is an error,
2181 returns the error, otherwise returns false.
2187 my $cust_pay = new FS::cust_pay ( {
2188 'invnum' => $self->invnum,
2189 'paid' => $self->owed,
2192 'payinfo' => $self->cust_main->payinfo,
2200 Attempts to pay this invoice with a credit card payment via a
2201 Business::OnlinePayment realtime gateway. See
2202 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2203 for supported processors.
2209 $self->realtime_bop( 'CC', @_ );
2214 Attempts to pay this invoice with an electronic check (ACH) payment via a
2215 Business::OnlinePayment realtime gateway. See
2216 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2217 for supported processors.
2223 $self->realtime_bop( 'ECHECK', @_ );
2228 Attempts to pay this invoice with phone bill (LEC) payment via a
2229 Business::OnlinePayment realtime gateway. See
2230 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2231 for supported processors.
2237 $self->realtime_bop( 'LEC', @_ );
2241 my( $self, $method ) = (shift,shift);
2242 my $conf = $self->conf;
2245 my $cust_main = $self->cust_main;
2246 my $balance = $cust_main->balance;
2247 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2248 $amount = sprintf("%.2f", $amount);
2249 return "not run (balance $balance)" unless $amount > 0;
2251 my $description = 'Internet Services';
2252 if ( $conf->exists('business-onlinepayment-description') ) {
2253 my $dtempl = $conf->config('business-onlinepayment-description');
2255 my $agent_obj = $cust_main->agent
2256 or die "can't retreive agent for $cust_main (agentnum ".
2257 $cust_main->agentnum. ")";
2258 my $agent = $agent_obj->agent;
2259 my $pkgs = join(', ',
2260 map { $_->part_pkg->pkg }
2261 grep { $_->pkgnum } $self->cust_bill_pkg
2263 $description = eval qq("$dtempl");
2266 $cust_main->realtime_bop($method, $amount,
2267 'description' => $description,
2268 'invnum' => $self->invnum,
2269 #this didn't do what we want, it just calls apply_payments_and_credits
2271 'apply_to_invoice' => 1,
2274 #this changes application behavior: auto payments
2275 #triggered against a specific invoice are now applied
2276 #to that invoice instead of oldest open.
2282 =item batch_card OPTION => VALUE...
2284 Adds a payment for this invoice to the pending credit card batch (see
2285 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2286 runs the payment using a realtime gateway.
2291 my ($self, %options) = @_;
2292 my $cust_main = $self->cust_main;
2294 $options{invnum} = $self->invnum;
2296 $cust_main->batch_card(%options);
2299 sub _agent_template {
2301 $self->cust_main->agent_template;
2304 sub _agent_invoice_from {
2306 $self->cust_main->agent_invoice_from;
2309 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2311 Returns an text invoice, as a list of lines.
2313 Options can be passed as a hashref (recommended) or as a list of time, template
2314 and then any key/value pairs for any other options.
2316 I<time>, if specified, is used to control the printing of overdue messages. The
2317 default is now. It isn't the date of the invoice; that's the `_date' field.
2318 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2319 L<Time::Local> and L<Date::Parse> for conversion functions.
2321 I<template>, if specified, is the name of a suffix for alternate invoices.
2323 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2329 my( $today, $template, %opt );
2331 %opt = %{ shift() };
2332 $today = delete($opt{'time'}) || '';
2333 $template = delete($opt{template}) || '';
2335 ( $today, $template, %opt ) = @_;
2338 my %params = ( 'format' => 'template' );
2339 $params{'time'} = $today if $today;
2340 $params{'template'} = $template if $template;
2341 $params{$_} = $opt{$_}
2342 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2344 $self->print_generic( %params );
2347 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2349 Internal method - returns a filename of a filled-in LaTeX template for this
2350 invoice (Note: add ".tex" to get the actual filename), and a filename of
2351 an associated logo (with the .eps extension included).
2353 See print_ps and print_pdf for methods that return PostScript and PDF output.
2355 Options can be passed as a hashref (recommended) or as a list of time, template
2356 and then any key/value pairs for any other options.
2358 I<time>, if specified, is used to control the printing of overdue messages. The
2359 default is now. It isn't the date of the invoice; that's the `_date' field.
2360 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2361 L<Time::Local> and L<Date::Parse> for conversion functions.
2363 I<template>, if specified, is the name of a suffix for alternate invoices.
2365 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2371 my $conf = $self->conf;
2372 my( $today, $template, %opt );
2374 %opt = %{ shift() };
2375 $today = delete($opt{'time'}) || '';
2376 $template = delete($opt{template}) || '';
2378 ( $today, $template, %opt ) = @_;
2381 my %params = ( 'format' => 'latex' );
2382 $params{'time'} = $today if $today;
2383 $params{'template'} = $template if $template;
2384 $params{$_} = $opt{$_}
2385 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2387 $template ||= $self->_agent_template;
2389 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2390 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2394 ) or die "can't open temp file: $!\n";
2396 my $agentnum = $self->cust_main->agentnum;
2398 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2399 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2400 or die "can't write temp file: $!\n";
2402 print $lh $conf->config_binary('logo.eps', $agentnum)
2403 or die "can't write temp file: $!\n";
2406 $params{'logo_file'} = $lh->filename;
2408 if($conf->exists('invoice-barcode')){
2409 my $png_file = $self->invoice_barcode($dir);
2410 my $eps_file = $png_file;
2411 $eps_file =~ s/\.png$/.eps/g;
2412 $png_file =~ /(barcode.*png)/;
2414 $eps_file =~ /(barcode.*eps)/;
2417 my $curr_dir = cwd();
2419 # after painfuly long experimentation, it was determined that sam2p won't
2420 # accept : and other chars in the path, no matter how hard I tried to
2421 # escape them, hence the chdir (and chdir back, just to be safe)
2422 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2423 or die "sam2p failed: $!\n";
2427 $params{'barcode_file'} = $eps_file;
2430 my @filled_in = $self->print_generic( %params );
2432 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2436 ) or die "can't open temp file: $!\n";
2437 binmode($fh, ':utf8'); # language support
2438 print $fh join('', @filled_in );
2441 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2442 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2446 =item invoice_barcode DIR_OR_FALSE
2448 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2449 it is taken as the temp directory where the PNG file will be generated and the
2450 PNG file name is returned. Otherwise, the PNG image itself is returned.
2454 sub invoice_barcode {
2455 my ($self, $dir) = (shift,shift);
2457 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2458 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2459 my $gd = $gdbar->plot(Height => 30);
2462 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2466 ) or die "can't open temp file: $!\n";
2467 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2468 my $png_file = $bh->filename;
2475 =item print_generic OPTION => VALUE ...
2477 Internal method - returns a filled-in template for this invoice as a scalar.
2479 See print_ps and print_pdf for methods that return PostScript and PDF output.
2481 Non optional options include
2482 format - latex, html, template
2484 Optional options include
2486 template - a value used as a suffix for a configuration template
2488 time - a value used to control the printing of overdue messages. The
2489 default is now. It isn't the date of the invoice; that's the `_date' field.
2490 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2491 L<Time::Local> and L<Date::Parse> for conversion functions.
2495 unsquelch_cdr - overrides any per customer cdr squelching when true
2497 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2499 locale - override customer's locale
2503 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2504 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2505 # yes: fixed width/plain text printing will be borked
2507 my( $self, %params ) = @_;
2508 my $conf = $self->conf;
2509 my $today = $params{today} ? $params{today} : time;
2510 warn "$me print_generic called on $self with suffix $params{template}\n"
2513 my $format = $params{format};
2514 die "Unknown format: $format"
2515 unless $format =~ /^(latex|html|template)$/;
2517 my $cust_main = $self->cust_main;
2518 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2519 unless $cust_main->payname
2520 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2522 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2523 'html' => [ '<%=', '%>' ],
2524 'template' => [ '{', '}' ],
2527 warn "$me print_generic creating template\n"
2530 #create the template
2531 my $template = $params{template} ? $params{template} : $self->_agent_template;
2532 my $templatefile = "invoice_$format";
2533 $templatefile .= "_$template"
2534 if length($template) && $conf->exists($templatefile."_$template");
2535 my @invoice_template = map "$_\n", $conf->config($templatefile)
2536 or die "cannot load config data $templatefile";
2539 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2540 #change this to a die when the old code is removed
2541 warn "old-style invoice template $templatefile; ".
2542 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2543 $old_latex = 'true';
2544 @invoice_template = _translate_old_latex_format(@invoice_template);
2547 warn "$me print_generic creating T:T object\n"
2550 my $text_template = new Text::Template(
2552 SOURCE => \@invoice_template,
2553 DELIMITERS => $delimiters{$format},
2556 warn "$me print_generic compiling T:T object\n"
2559 $text_template->compile()
2560 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2563 # additional substitution could possibly cause breakage in existing templates
2564 my %convert_maps = (
2566 'notes' => sub { map "$_", @_ },
2567 'footer' => sub { map "$_", @_ },
2568 'smallfooter' => sub { map "$_", @_ },
2569 'returnaddress' => sub { map "$_", @_ },
2570 'coupon' => sub { map "$_", @_ },
2571 'summary' => sub { map "$_", @_ },
2577 s/%%(.*)$/<!-- $1 -->/g;
2578 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2579 s/\\begin\{enumerate\}/<ol>/g;
2581 s/\\end\{enumerate\}/<\/ol>/g;
2582 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2591 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2593 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2598 s/\\\\\*?\s*$/<BR>/;
2599 s/\\hyphenation\{[\w\s\-]+}//;
2604 'coupon' => sub { "" },
2605 'summary' => sub { "" },
2612 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2613 s/\\begin\{enumerate\}//g;
2615 s/\\end\{enumerate\}//g;
2616 s/\\textbf\{(.*)\}/$1/g;
2623 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2625 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2630 s/\\\\\*?\s*$/\n/; # dubious
2631 s/\\hyphenation\{[\w\s\-]+}//;
2635 'coupon' => sub { "" },
2636 'summary' => sub { "" },
2641 # hashes for differing output formats
2642 my %nbsps = ( 'latex' => '~',
2643 'html' => '', # '&nbps;' would be nice
2644 'template' => '', # not used
2646 my $nbsp = $nbsps{$format};
2648 my %escape_functions = ( 'latex' => \&_latex_escape,
2649 'html' => \&_html_escape_nbsp,#\&encode_entities,
2650 'template' => sub { shift },
2652 my $escape_function = $escape_functions{$format};
2653 my $escape_function_nonbsp = ($format eq 'html')
2654 ? \&_html_escape : $escape_function;
2656 my %date_formats = ( 'latex' => $date_format_long,
2657 'html' => $date_format_long,
2660 $date_formats{'html'} =~ s/ / /g;
2662 my $date_format = $date_formats{$format};
2664 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2666 'html' => sub { return '<b>'. shift(). '</b>'
2668 'template' => sub { shift },
2670 my $embolden_function = $embolden_functions{$format};
2672 my %newline_tokens = ( 'latex' => '\\\\',
2676 my $newline_token = $newline_tokens{$format};
2678 warn "$me generating template variables\n"
2681 # generate template variables
2684 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2688 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2694 $returnaddress = join("\n",
2695 $conf->config_orbase("invoice_${format}returnaddress", $template)
2698 } elsif ( grep /\S/,
2699 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2701 my $convert_map = $convert_maps{$format}{'returnaddress'};
2704 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2709 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2711 my $convert_map = $convert_maps{$format}{'returnaddress'};
2712 $returnaddress = join( "\n", &$convert_map(
2713 map { s/( {2,})/'~' x length($1)/eg;
2717 ( $conf->config('company_name', $self->cust_main->agentnum),
2718 $conf->config('company_address', $self->cust_main->agentnum),
2725 my $warning = "Couldn't find a return address; ".
2726 "do you need to set the company_address configuration value?";
2728 $returnaddress = $nbsp;
2729 #$returnaddress = $warning;
2733 warn "$me generating invoice data\n"
2736 my $agentnum = $self->cust_main->agentnum;
2738 my %invoice_data = (
2741 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2742 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2743 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2744 'returnaddress' => $returnaddress,
2745 'agent' => &$escape_function($cust_main->agent->agent),
2748 'invnum' => $self->invnum,
2749 '_date' => $self->_date,
2750 'date' => $self->time2str_local($date_format, $self->_date),
2751 'today' => $self->time2str_local($date_format_long, $today),
2752 'terms' => $self->terms,
2753 'template' => $template, #params{'template'},
2754 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2755 'current_charges' => sprintf("%.2f", $self->charged),
2756 'duedate' => $self->due_date2str($rdate_format), #date_format?
2759 'custnum' => $cust_main->display_custnum,
2760 'agent_custid' => &$escape_function($cust_main->agent_custid),
2761 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2762 payname company address1 address2 city state zip fax
2766 'ship_enable' => $conf->exists('invoice-ship_address'),
2767 'unitprices' => $conf->exists('invoice-unitprice'),
2768 'smallernotes' => $conf->exists('invoice-smallernotes'),
2769 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2770 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2772 #layout info -- would be fancy to calc some of this and bury the template
2774 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2775 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2776 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2777 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2778 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2779 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2780 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2781 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2782 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2783 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2785 # better hang on to conf_dir for a while (for old templates)
2786 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2788 #these are only used when doing paged plaintext
2794 #localization (see FS::cust_main_Mixin)
2795 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2796 # prototype here to silence warnings
2797 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_) };
2799 my $min_sdate = 999999999999;
2801 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2802 next unless $cust_bill_pkg->pkgnum > 0;
2803 $min_sdate = $cust_bill_pkg->sdate
2804 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2805 $max_edate = $cust_bill_pkg->edate
2806 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2809 $invoice_data{'bill_period'} = '';
2810 $invoice_data{'bill_period'} = $self->time2str_local('%e %h', $min_sdate)
2812 $self->time2str_local('%e %h', $max_edate)
2813 if ($max_edate != 0 && $min_sdate != 999999999999);
2815 $invoice_data{finance_section} = '';
2816 if ( $conf->config('finance_pkgclass') ) {
2818 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2819 $invoice_data{finance_section} = $pkg_class->categoryname;
2821 $invoice_data{finance_amount} = '0.00';
2822 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2824 my $countrydefault = $conf->config('countrydefault') || 'US';
2825 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2826 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2827 my $method = $prefix.$_;
2828 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2830 $invoice_data{'ship_country'} = ''
2831 if ( $invoice_data{'ship_country'} eq $countrydefault );
2833 $invoice_data{'cid'} = $params{'cid'}
2836 if ( $cust_main->country eq $countrydefault ) {
2837 $invoice_data{'country'} = '';
2839 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2843 $invoice_data{'address'} = \@address;
2845 $cust_main->payname.
2846 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2847 ? " (P.O. #". $cust_main->payinfo. ")"
2851 push @address, $cust_main->company
2852 if $cust_main->company;
2853 push @address, $cust_main->address1;
2854 push @address, $cust_main->address2
2855 if $cust_main->address2;
2857 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2858 push @address, $invoice_data{'country'}
2859 if $invoice_data{'country'};
2861 while (scalar(@address) < 5);
2863 $invoice_data{'logo_file'} = $params{'logo_file'}
2864 if $params{'logo_file'};
2865 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2866 if $params{'barcode_file'};
2867 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2868 if $params{'barcode_img'};
2869 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2870 if $params{'barcode_cid'};
2872 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2873 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2874 #my $balance_due = $self->owed + $pr_total - $cr_total;
2875 my $balance_due = $self->owed + $pr_total;
2877 # the customer's current balance as shown on the invoice before this one
2878 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2880 # the change in balance from that invoice to this one
2881 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2883 # the sum of amount owed on all previous invoices
2884 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2886 # the sum of amount owed on all invoices
2887 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2889 # info from customer's last invoice before this one, for some
2891 $invoice_data{'last_bill'} = {};
2892 if ( $self->previous_bill ) {
2893 $invoice_data{'last_bill'} = {
2894 '_date' => $self->previous_bill->_date, #unformatted
2895 # all we need for now
2899 my $summarypage = '';
2900 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2903 $invoice_data{'summarypage'} = $summarypage;
2905 warn "$me substituting variables in notes, footer, smallfooter\n"
2908 my @include = (qw( notes footer smallfooter ));
2909 push @include, 'coupon' unless $params{'no_coupon'};
2910 foreach my $include (@include) {
2912 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2915 if ( $conf->exists($inc_file, $agentnum)
2916 && length( $conf->config($inc_file, $agentnum) ) ) {
2918 @inc_src = $conf->config($inc_file, $agentnum);
2922 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2924 my $convert_map = $convert_maps{$format}{$include};
2926 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2927 s/--\@\]/$delimiters{$format}[1]/g;
2930 &$convert_map( $conf->config($inc_file, $agentnum) );
2934 my $inc_tt = new Text::Template (
2936 SOURCE => [ map "$_\n", @inc_src ],
2937 DELIMITERS => $delimiters{$format},
2938 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2940 unless ( $inc_tt->compile() ) {
2941 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2942 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2946 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2948 $invoice_data{$include} =~ s/\n+$//
2949 if ($format eq 'latex');
2952 # let invoices use either of these as needed
2953 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2954 ? $cust_main->payinfo : '';
2955 $invoice_data{'po_line'} =
2956 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2957 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2960 my %money_chars = ( 'latex' => '',
2961 'html' => $conf->config('money_char') || '$',
2964 my $money_char = $money_chars{$format};
2966 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2967 'html' => $conf->config('money_char') || '$',
2970 my $other_money_char = $other_money_chars{$format};
2971 $invoice_data{'dollar'} = $other_money_char;
2973 my @detail_items = ();
2974 my @total_items = ();
2978 $invoice_data{'detail_items'} = \@detail_items;
2979 $invoice_data{'total_items'} = \@total_items;
2980 $invoice_data{'buf'} = \@buf;
2981 $invoice_data{'sections'} = \@sections;
2983 warn "$me generating sections\n"
2986 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2987 'subtotal' => $other_money_char.
2988 sprintf('%.2f', $pr_total),
2989 'summarized' => '', #why? $summarypage ? 'Y' : '',
2991 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2992 join(' / ', map { $cust_main->balance_date_range(@$_) }
2993 $self->_prior_month30s
2995 if $conf->exists('invoice_include_aging');
2998 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2999 'subtotal' => $taxtotal, # adjusted below
3002 my $tax_weight = _pkg_category($tax_section->{description})
3003 ? _pkg_category($tax_section->{description})->weight
3005 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3006 $tax_section->{'sort_weight'} = $tax_weight;
3009 my $adjusttotal = 0;
3010 my $adjust_section = {
3011 'description' => $self->mt('Credits, Payments, and Adjustments'),
3012 'adjust_section' => 1,
3013 'subtotal' => 0, # adjusted below
3015 my $adjust_weight = _pkg_category($adjust_section->{description})
3016 ? _pkg_category($adjust_section->{description})->weight
3018 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3019 $adjust_section->{'sort_weight'} = $adjust_weight;
3021 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3022 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3023 $invoice_data{'multisection'} = $multisection;
3024 my $late_sections = [];
3025 my $extra_sections = [];
3026 my $extra_lines = ();
3028 my $default_section = { 'description' => '',
3033 if ( $multisection ) {
3034 ($extra_sections, $extra_lines) =
3035 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3036 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3038 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3040 push @detail_items, @$extra_lines if $extra_lines;
3042 $self->_items_sections( $late_sections, # this could stand a refactor
3044 $escape_function_nonbsp,
3048 if ($conf->exists('svc_phone_sections')) {
3049 my ($phone_sections, $phone_lines) =
3050 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3051 push @{$late_sections}, @$phone_sections;
3052 push @detail_items, @$phone_lines;
3054 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3055 my ($accountcode_section, $accountcode_lines) =
3056 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3057 if ( scalar(@$accountcode_lines) ) {
3058 push @{$late_sections}, $accountcode_section;
3059 push @detail_items, @$accountcode_lines;
3062 } else {# not multisection
3063 # make a default section
3064 push @sections, $default_section;
3065 # and calculate the finance charge total, since it won't get done otherwise.
3066 # XXX possibly other totals?
3067 # XXX possibly finance_pkgclass should not be used in this manner?
3068 if ( $conf->exists('finance_pkgclass') ) {
3069 my @finance_charges;
3070 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3071 if ( grep { $_->section eq $invoice_data{finance_section} }
3072 $cust_bill_pkg->cust_bill_pkg_display ) {
3073 # I think these are always setup fees, but just to be sure...
3074 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3077 $invoice_data{finance_amount} =
3078 sprintf('%.2f', sum( @finance_charges ) || 0);
3082 # previous invoice balances in the Previous Charges section if there
3083 # is one, otherwise in the main detail section
3084 if ( $self->can('_items_previous') &&
3085 $self->enable_previous &&
3086 ! $conf->exists('previous_balance-summary_only') ) {
3088 warn "$me adding previous balances\n"
3091 foreach my $line_item ( $self->_items_previous ) {
3094 ext_description => [],
3096 $detail->{'ref'} = $line_item->{'pkgnum'};
3097 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3098 $detail->{'quantity'} = 1;
3099 $detail->{'section'} = $multisection ? $previous_section
3101 $detail->{'description'} = &$escape_function($line_item->{'description'});
3102 if ( exists $line_item->{'ext_description'} ) {
3103 @{$detail->{'ext_description'}} = map {
3104 &$escape_function($_);
3105 } @{$line_item->{'ext_description'}};
3107 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3108 $line_item->{'amount'};
3109 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3111 push @detail_items, $detail;
3112 push @buf, [ $detail->{'description'},
3113 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3119 if ( @pr_cust_bill && $self->enable_previous ) {
3120 push @buf, ['','-----------'];
3121 push @buf, [ $self->mt('Total Previous Balance'),
3122 $money_char. sprintf("%10.2f", $pr_total) ];
3126 if ( $conf->exists('svc_phone-did-summary') ) {
3127 warn "$me adding DID summary\n"
3130 my ($didsummary,$minutes) = $self->_did_summary;
3131 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3133 { 'description' => $didsummary_desc,
3134 'ext_description' => [ $didsummary, $minutes ],
3138 foreach my $section (@sections, @$late_sections) {
3140 warn "$me adding section \n". Dumper($section)
3143 # begin some normalization
3144 $section->{'subtotal'} = $section->{'amount'}
3146 && !exists($section->{subtotal})
3147 && exists($section->{amount});
3149 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3150 if ( $invoice_data{finance_section} &&
3151 $section->{'description'} eq $invoice_data{finance_section} );
3153 $section->{'subtotal'} = $other_money_char.
3154 sprintf('%.2f', $section->{'subtotal'})
3157 # continue some normalization
3158 $section->{'amount'} = $section->{'subtotal'}
3162 if ( $section->{'description'} ) {
3163 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3168 warn "$me setting options\n"
3171 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3173 $options{'section'} = $section if $multisection;
3174 $options{'format'} = $format;
3175 $options{'escape_function'} = $escape_function;
3176 $options{'no_usage'} = 1 unless $unsquelched;
3177 $options{'unsquelched'} = $unsquelched;
3178 $options{'summary_page'} = $summarypage;
3179 $options{'skip_usage'} =
3180 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3181 $options{'multilocation'} = $multilocation;
3182 $options{'multisection'} = $multisection;
3184 warn "$me searching for line items\n"
3187 foreach my $line_item ( $self->_items_pkg(%options) ) {
3189 warn "$me adding line item $line_item\n"
3193 ext_description => [],
3195 $detail->{'ref'} = $line_item->{'pkgnum'};
3196 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3197 $detail->{'quantity'} = $line_item->{'quantity'};
3198 $detail->{'section'} = $section;
3199 $detail->{'description'} = &$escape_function($line_item->{'description'});
3200 if ( exists $line_item->{'ext_description'} ) {
3201 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3203 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3204 $line_item->{'amount'};
3205 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3206 $line_item->{'unit_amount'};
3207 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3209 $detail->{'sdate'} = $line_item->{'sdate'};
3210 $detail->{'edate'} = $line_item->{'edate'};
3211 $detail->{'seconds'} = $line_item->{'seconds'};
3212 $detail->{'svc_label'} = $line_item->{'svc_label'};
3214 push @detail_items, $detail;
3215 push @buf, ( [ $detail->{'description'},
3216 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3218 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3222 if ( $section->{'description'} ) {
3223 push @buf, ( ['','-----------'],
3224 [ $section->{'description'}. ' sub-total',
3225 $section->{'subtotal'} # already formatted this
3234 $invoice_data{current_less_finance} =
3235 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3237 # create a major section for previous balance if we have major sections,
3238 # or if previous_section is in summary form
3239 if ( ( $multisection && $self->enable_previous )
3240 || $conf->exists('previous_balance-summary_only') )
3242 unshift @sections, $previous_section if $pr_total;
3245 warn "$me adding taxes\n"
3248 foreach my $tax ( $self->_items_tax ) {
3250 $taxtotal += $tax->{'amount'};
3252 my $description = &$escape_function( $tax->{'description'} );
3253 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3255 if ( $multisection ) {
3257 my $money = $old_latex ? '' : $money_char;
3258 push @detail_items, {
3259 ext_description => [],
3262 description => $description,
3263 amount => $money. $amount,
3265 section => $tax_section,
3270 push @total_items, {
3271 'total_item' => $description,
3272 'total_amount' => $other_money_char. $amount,
3277 push @buf,[ $description,
3278 $money_char. $amount,
3285 $total->{'total_item'} = $self->mt('Sub-total');
3286 $total->{'total_amount'} =
3287 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3289 if ( $multisection ) {
3290 $tax_section->{'subtotal'} = $other_money_char.
3291 sprintf('%.2f', $taxtotal);
3292 $tax_section->{'pretotal'} = 'New charges sub-total '.
3293 $total->{'total_amount'};
3294 push @sections, $tax_section if $taxtotal;
3296 unshift @total_items, $total;
3299 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3301 push @buf,['','-----------'];
3302 push @buf,[$self->mt(
3303 (!$self->enable_previous)
3305 : 'Total New Charges'
3307 $money_char. sprintf("%10.2f",$self->charged) ];
3310 # calculate total, possibly including total owed on previous
3315 $item = $conf->config('previous_balance-exclude_from_total')
3316 || 'Total New Charges'
3317 if $conf->exists('previous_balance-exclude_from_total');
3318 my $amount = $self->charged;
3319 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3320 $amount += $pr_total;
3323 $total->{'total_item'} = &$embolden_function($self->mt($item));
3324 $total->{'total_amount'} =
3325 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3326 if ( $multisection ) {
3327 if ( $adjust_section->{'sort_weight'} ) {
3328 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3329 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3331 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3332 $other_money_char. sprintf('%.2f', $self->charged );
3335 push @total_items, $total;
3337 push @buf,['','-----------'];
3340 sprintf( '%10.2f', $amount )
3345 # if we're showing previous invoices, also show previous
3346 # credits and payments
3347 if ( $self->enable_previous
3348 and $self->can('_items_credits')
3349 and $self->can('_items_payments') )
3351 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3354 my $credittotal = 0;
3355 foreach my $credit (
3356 $self->_items_credits( 'template' => $template, 'trim_len' => 60)
3360 $total->{'total_item'} = &$escape_function($credit->{'description'});
3361 $credittotal += $credit->{'amount'};
3362 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3363 $adjusttotal += $credit->{'amount'};
3364 if ( $multisection ) {
3365 my $money = $old_latex ? '' : $money_char;
3366 push @detail_items, {
3367 ext_description => [],
3370 description => &$escape_function($credit->{'description'}),
3371 amount => $money. $credit->{'amount'},
3373 section => $adjust_section,
3376 push @total_items, $total;
3380 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3383 foreach my $credit (
3384 $self->_items_credits( 'template' => $template, 'trim_len' => 32)
3386 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3390 my $paymenttotal = 0;
3391 foreach my $payment (
3392 $self->_items_payments( 'template' => $template )
3395 $total->{'total_item'} = &$escape_function($payment->{'description'});
3396 $paymenttotal += $payment->{'amount'};
3397 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3398 $adjusttotal += $payment->{'amount'};
3399 if ( $multisection ) {
3400 my $money = $old_latex ? '' : $money_char;
3401 push @detail_items, {
3402 ext_description => [],
3405 description => &$escape_function($payment->{'description'}),
3406 amount => $money. $payment->{'amount'},
3408 section => $adjust_section,
3411 push @total_items, $total;
3413 push @buf, [ $payment->{'description'},
3414 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3417 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3419 if ( $multisection ) {
3420 $adjust_section->{'subtotal'} = $other_money_char.
3421 sprintf('%.2f', $adjusttotal);
3422 push @sections, $adjust_section
3423 unless $adjust_section->{sort_weight};
3426 # create Balance Due message
3429 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3430 $total->{'total_amount'} =
3431 &$embolden_function(
3432 $other_money_char. sprintf('%.2f', #why? $summarypage
3433 # ? $self->charged +
3434 # $self->billing_balance
3436 $self->owed + $pr_total
3439 if ( $multisection && !$adjust_section->{sort_weight} ) {
3440 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3441 $total->{'total_amount'};
3443 push @total_items, $total;
3445 push @buf,['','-----------'];
3446 push @buf,[$self->balance_due_msg, $money_char.
3447 sprintf("%10.2f", $balance_due ) ];
3450 if ( $conf->exists('previous_balance-show_credit')
3451 and $cust_main->balance < 0 ) {
3452 my $credit_total = {
3453 'total_item' => &$embolden_function($self->credit_balance_msg),
3454 'total_amount' => &$embolden_function(
3455 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3458 if ( $multisection ) {
3459 $adjust_section->{'posttotal'} .= $newline_token .
3460 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3463 push @total_items, $credit_total;
3465 push @buf,['','-----------'];
3466 push @buf,[$self->credit_balance_msg, $money_char.
3467 sprintf("%10.2f", -$cust_main->balance ) ];
3471 if ( $multisection ) {
3472 if ($conf->exists('svc_phone_sections')) {
3474 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3475 $total->{'total_amount'} =
3476 &$embolden_function(
3477 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3479 my $last_section = pop @sections;
3480 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3481 $total->{'total_amount'};
3482 push @sections, $last_section;
3484 push @sections, @$late_sections
3488 # make a discounts-available section, even without multisection
3489 if ( $conf->exists('discount-show_available')
3490 and my @discounts_avail = $self->_items_discounts_avail ) {
3491 my $discount_section = {
3492 'description' => $self->mt('Discounts Available'),
3497 push @sections, $discount_section;
3498 push @detail_items, map { +{
3499 'ref' => '', #should this be something else?
3500 'section' => $discount_section,
3501 'description' => &$escape_function( $_->{description} ),
3502 'amount' => $money_char . &$escape_function( $_->{amount} ),
3503 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3504 } } @discounts_avail;
3507 # debugging hook: call this with 'diag' => 1 to just get a hash of
3508 # the invoice variables
3509 return \%invoice_data if ( $params{'diag'} );
3511 # All sections and items are built; now fill in templates.
3512 my @includelist = ();
3513 push @includelist, 'summary' if $summarypage;
3514 foreach my $include ( @includelist ) {
3516 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3519 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3521 @inc_src = $conf->config($inc_file, $agentnum);
3525 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3527 my $convert_map = $convert_maps{$format}{$include};
3529 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3530 s/--\@\]/$delimiters{$format}[1]/g;
3533 &$convert_map( $conf->config($inc_file, $agentnum) );
3537 my $inc_tt = new Text::Template (
3539 SOURCE => [ map "$_\n", @inc_src ],
3540 DELIMITERS => $delimiters{$format},
3541 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3543 unless ( $inc_tt->compile() ) {
3544 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3545 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3549 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3551 $invoice_data{$include} =~ s/\n+$//
3552 if ($format eq 'latex');
3557 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3558 /invoice_lines\((\d*)\)/;
3559 $invoice_lines += $1 || scalar(@buf);
3562 die "no invoice_lines() functions in template?"
3563 if ( $format eq 'template' && !$wasfunc );
3565 if ($format eq 'template') {
3567 if ( $invoice_lines ) {
3568 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3569 $invoice_data{'total_pages'}++
3570 if scalar(@buf) % $invoice_lines;
3573 #setup subroutine for the template
3574 $invoice_data{invoice_lines} = sub {
3575 my $lines = shift || scalar(@buf);
3587 push @collect, split("\n",
3588 $text_template->fill_in( HASH => \%invoice_data )
3590 $invoice_data{'page'}++;
3592 map "$_\n", @collect;
3594 # this is where we actually create the invoice
3595 warn "filling in template for invoice ". $self->invnum. "\n"
3597 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3600 $text_template->fill_in(HASH => \%invoice_data);
3604 # helper routine for generating date ranges
3605 sub _prior_month30s {
3608 [ 1, 2592000 ], # 0-30 days ago
3609 [ 2592000, 5184000 ], # 30-60 days ago
3610 [ 5184000, 7776000 ], # 60-90 days ago
3611 [ 7776000, 0 ], # 90+ days ago
3614 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3615 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3620 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3622 Returns an postscript invoice, as a scalar.
3624 Options can be passed as a hashref (recommended) or as a list of time, template
3625 and then any key/value pairs for any other options.
3627 I<time> an optional value used to control the printing of overdue messages. The
3628 default is now. It isn't the date of the invoice; that's the `_date' field.
3629 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3630 L<Time::Local> and L<Date::Parse> for conversion functions.
3632 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3639 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3640 my $ps = generate_ps($file);
3642 unlink($barcodefile) if $barcodefile;
3647 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3649 Returns an PDF invoice, as a scalar.
3651 Options can be passed as a hashref (recommended) or as a list of time, template
3652 and then any key/value pairs for any other options.
3654 I<time> an optional value used to control the printing of overdue messages. The
3655 default is now. It isn't the date of the invoice; that's the `_date' field.
3656 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3657 L<Time::Local> and L<Date::Parse> for conversion functions.
3659 I<template>, if specified, is the name of a suffix for alternate invoices.
3661 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3668 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3669 my $pdf = generate_pdf($file);
3671 unlink($barcodefile) if $barcodefile;
3676 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3678 Returns an HTML invoice, as a scalar.
3680 I<time> an optional value used to control the printing of overdue messages. The
3681 default is now. It isn't the date of the invoice; that's the `_date' field.
3682 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3683 L<Time::Local> and L<Date::Parse> for conversion functions.
3685 I<template>, if specified, is the name of a suffix for alternate invoices.
3687 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3689 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3690 when emailing the invoice as part of a multipart/related MIME email.
3698 %params = %{ shift() };
3700 $params{'time'} = shift;
3701 $params{'template'} = shift;
3702 $params{'cid'} = shift;
3705 $params{'format'} = 'html';
3707 $self->print_generic( %params );
3710 # quick subroutine for print_latex
3712 # There are ten characters that LaTeX treats as special characters, which
3713 # means that they do not simply typeset themselves:
3714 # # $ % & ~ _ ^ \ { }
3716 # TeX ignores blanks following an escaped character; if you want a blank (as
3717 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3721 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3722 $value =~ s/([<>])/\$$1\$/g;
3728 encode_entities($value);
3732 sub _html_escape_nbsp {
3733 my $value = _html_escape(shift);
3734 $value =~ s/ +/ /g;
3738 #utility methods for print_*
3740 sub _translate_old_latex_format {
3741 warn "_translate_old_latex_format called\n"
3748 if ( $line =~ /^%%Detail\s*$/ ) {
3750 push @template, q![@--!,
3751 q! foreach my $_tr_line (@detail_items) {!,
3752 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3753 q! $_tr_line->{'description'} .= !,
3754 q! "\\tabularnewline\n~~".!,
3755 q! join( "\\tabularnewline\n~~",!,
3756 q! @{$_tr_line->{'ext_description'}}!,
3760 while ( ( my $line_item_line = shift )
3761 !~ /^%%EndDetail\s*$/ ) {
3762 $line_item_line =~ s/'/\\'/g; # nice LTS
3763 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3764 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3765 push @template, " \$OUT .= '$line_item_line';";
3768 push @template, '}',
3771 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3773 push @template, '[@--',
3774 ' foreach my $_tr_line (@total_items) {';
3776 while ( ( my $total_item_line = shift )
3777 !~ /^%%EndTotalDetails\s*$/ ) {
3778 $total_item_line =~ s/'/\\'/g; # nice LTS
3779 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3780 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3781 push @template, " \$OUT .= '$total_item_line';";
3784 push @template, '}',
3788 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3789 push @template, $line;
3795 warn "$_\n" foreach @template;
3803 my $conf = $self->conf;
3805 #check for an invoice-specific override
3806 return $self->invoice_terms if $self->invoice_terms;
3808 #check for a customer- specific override
3809 my $cust_main = $self->cust_main;
3810 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3812 #use configured default
3813 $conf->config('invoice_default_terms') || '';
3819 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3820 $duedate = $self->_date() + ( $1 * 86400 );
3827 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
3830 sub balance_due_msg {
3832 my $msg = $self->mt('Balance Due');
3833 return $msg unless $self->terms;
3834 if ( $self->due_date ) {
3835 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3836 $self->due_date2str($date_format);
3837 } elsif ( $self->terms ) {
3838 $msg .= ' - '. $self->terms;
3843 sub balance_due_date {
3845 my $conf = $self->conf;
3847 if ( $conf->exists('invoice_default_terms')
3848 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3849 $duedate = $self->time2str_local($rdate_format, $self->_date + ($1*86400) );
3854 sub credit_balance_msg {
3856 $self->mt('Credit Balance Remaining')
3859 =item invnum_date_pretty
3861 Returns a string with the invoice number and date, for example:
3862 "Invoice #54 (3/20/2008)"
3866 sub invnum_date_pretty {
3868 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3873 Returns a string with the date, for example: "3/20/2008"
3879 $self->time2str_local($date_format, $self->_date);
3882 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3884 Generate section information for all items appearing on this invoice.
3885 This will only be called for multi-section invoices.
3887 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3888 related display records (L<FS::cust_bill_pkg_display>) and organize
3889 them into two groups ("early" and "late" according to whether they come
3890 before or after the total), then into sections. A subtotal is calculated
3893 Section descriptions are returned in sort weight order. Each consists
3894 of a hash containing:
3896 description: the package category name, escaped
3897 subtotal: the total charges in that section
3898 tax_section: a flag indicating that the section contains only tax charges
3899 summarized: same as tax_section, for some reason
3900 sort_weight: the package category's sort weight
3902 If 'condense' is set on the display record, it also contains everything
3903 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3904 coderefs to generate parts of the invoice. This is not advised.
3908 LATE: an arrayref to push the "late" section hashes onto. The "early"
3909 group is simply returned from the method.
3911 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3912 Turning this on has the following effects:
3913 - Ignores display items with the 'summary' flag.
3914 - Combines all items into the "early" group.
3915 - Creates sections for all non-disabled package categories, even if they
3916 have no charges on this invoice, as well as a section with no name.
3918 ESCAPE: an escape function to use for section titles.
3920 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3921 sorted list. If there are any of these, section subtotals exclude
3924 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3925 passed through to C<_condense_section()>.
3929 use vars qw(%pkg_category_cache);
3930 sub _items_sections {
3933 my $summarypage = shift;
3935 my $extra_sections = shift;
3939 my %late_subtotal = ();
3942 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3945 my $usage = $cust_bill_pkg->usage;
3947 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3948 next if ( $display->summary && $summarypage );
3950 my $section = $display->section;
3951 my $type = $display->type;
3953 $not_tax{$section} = 1
3954 unless $cust_bill_pkg->pkgnum == 0;
3956 if ( $display->post_total && !$summarypage ) {
3957 if (! $type || $type eq 'S') {
3958 $late_subtotal{$section} += $cust_bill_pkg->setup
3959 if $cust_bill_pkg->setup != 0
3960 || $cust_bill_pkg->setup_show_zero;
3964 $late_subtotal{$section} += $cust_bill_pkg->recur
3965 if $cust_bill_pkg->recur != 0
3966 || $cust_bill_pkg->recur_show_zero;
3969 if ($type && $type eq 'R') {
3970 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3971 if $cust_bill_pkg->recur != 0
3972 || $cust_bill_pkg->recur_show_zero;
3975 if ($type && $type eq 'U') {
3976 $late_subtotal{$section} += $usage
3977 unless scalar(@$extra_sections);
3982 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3984 if (! $type || $type eq 'S') {
3985 $subtotal{$section} += $cust_bill_pkg->setup
3986 if $cust_bill_pkg->setup != 0
3987 || $cust_bill_pkg->setup_show_zero;
3991 $subtotal{$section} += $cust_bill_pkg->recur
3992 if $cust_bill_pkg->recur != 0
3993 || $cust_bill_pkg->recur_show_zero;
3996 if ($type && $type eq 'R') {
3997 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3998 if $cust_bill_pkg->recur != 0
3999 || $cust_bill_pkg->recur_show_zero;
4002 if ($type && $type eq 'U') {
4003 $subtotal{$section} += $usage
4004 unless scalar(@$extra_sections);
4013 %pkg_category_cache = ();
4015 push @$late, map { { 'description' => &{$escape}($_),
4016 'subtotal' => $late_subtotal{$_},
4018 'sort_weight' => ( _pkg_category($_)
4019 ? _pkg_category($_)->weight
4022 ((_pkg_category($_) && _pkg_category($_)->condense)
4023 ? $self->_condense_section($format)
4027 sort _sectionsort keys %late_subtotal;
4030 if ( $summarypage ) {
4031 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4032 map { $_->categoryname } qsearch('pkg_category', {});
4033 push @sections, '' if exists($subtotal{''});
4035 @sections = keys %subtotal;
4038 my @early = map { { 'description' => &{$escape}($_),
4039 'subtotal' => $subtotal{$_},
4040 'summarized' => $not_tax{$_} ? '' : 'Y',
4041 'tax_section' => $not_tax{$_} ? '' : 'Y',
4042 'sort_weight' => ( _pkg_category($_)
4043 ? _pkg_category($_)->weight
4046 ((_pkg_category($_) && _pkg_category($_)->condense)
4047 ? $self->_condense_section($format)
4052 push @early, @$extra_sections if $extra_sections;
4054 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4058 #helper subs for above
4061 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4065 my $categoryname = shift;
4066 $pkg_category_cache{$categoryname} ||=
4067 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4070 my %condensed_format = (
4071 'label' => [ qw( Description Qty Amount ) ],
4073 sub { shift->{description} },
4074 sub { shift->{quantity} },
4075 sub { my($href, %opt) = @_;
4076 ($opt{dollar} || ''). $href->{amount};
4079 'align' => [ qw( l r r ) ],
4080 'span' => [ qw( 5 1 1 ) ], # unitprices?
4081 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4084 sub _condense_section {
4085 my ( $self, $format ) = ( shift, shift );
4087 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4088 qw( description_generator
4091 total_line_generator
4096 sub _condensed_generator_defaults {
4097 my ( $self, $format ) = ( shift, shift );
4098 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4107 sub _condensed_header_generator {
4108 my ( $self, $format ) = ( shift, shift );
4110 my ( $f, $prefix, $suffix, $separator, $column ) =
4111 _condensed_generator_defaults($format);
4113 if ($format eq 'latex') {
4114 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4115 $suffix = "\\\\\n\\hline";
4118 sub { my ($d,$a,$s,$w) = @_;
4119 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4121 } elsif ( $format eq 'html' ) {
4122 $prefix = '<th></th>';
4126 sub { my ($d,$a,$s,$w) = @_;
4127 return qq!<th align="$html_align{$a}">$d</th>!;
4135 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4137 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4140 $prefix. join($separator, @result). $suffix;
4145 sub _condensed_description_generator {
4146 my ( $self, $format ) = ( shift, shift );
4148 my ( $f, $prefix, $suffix, $separator, $column ) =
4149 _condensed_generator_defaults($format);
4151 my $money_char = '$';
4152 if ($format eq 'latex') {
4153 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4155 $separator = " & \n";
4157 sub { my ($d,$a,$s,$w) = @_;
4158 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4160 $money_char = '\\dollar';
4161 }elsif ( $format eq 'html' ) {
4162 $prefix = '"><td align="center"></td>';
4166 sub { my ($d,$a,$s,$w) = @_;
4167 return qq!<td align="$html_align{$a}">$d</td>!;
4169 #$money_char = $conf->config('money_char') || '$';
4170 $money_char = ''; # this is madness
4178 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4180 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4182 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4183 map { $f->{$_}->[$i] } qw(align span width)
4187 $prefix. join( $separator, @result ). $suffix;
4192 sub _condensed_total_generator {
4193 my ( $self, $format ) = ( shift, shift );
4195 my ( $f, $prefix, $suffix, $separator, $column ) =
4196 _condensed_generator_defaults($format);
4199 if ($format eq 'latex') {
4202 $separator = " & \n";
4204 sub { my ($d,$a,$s,$w) = @_;
4205 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4207 }elsif ( $format eq 'html' ) {
4211 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4213 sub { my ($d,$a,$s,$w) = @_;
4214 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4223 # my $r = &{$f->{fields}->[$i]}(@args);
4224 # $r .= ' Total' unless $i;
4226 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4228 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4229 map { $f->{$_}->[$i] } qw(align span width)
4233 $prefix. join( $separator, @result ). $suffix;
4238 =item total_line_generator FORMAT
4240 Returns a coderef used for generation of invoice total line items for this
4241 usage_class. FORMAT is either html or latex
4245 # should not be used: will have issues with hash element names (description vs
4246 # total_item and amount vs total_amount -- another array of functions?
4248 sub _condensed_total_line_generator {
4249 my ( $self, $format ) = ( shift, shift );
4251 my ( $f, $prefix, $suffix, $separator, $column ) =
4252 _condensed_generator_defaults($format);
4255 if ($format eq 'latex') {
4258 $separator = " & \n";
4260 sub { my ($d,$a,$s,$w) = @_;
4261 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4263 }elsif ( $format eq 'html' ) {
4267 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4269 sub { my ($d,$a,$s,$w) = @_;
4270 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4279 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4281 &{$column}( &{$f->{fields}->[$i]}(@args),
4282 map { $f->{$_}->[$i] } qw(align span width)
4286 $prefix. join( $separator, @result ). $suffix;
4291 #sub _items_extra_usage_sections {
4293 # my $escape = shift;
4295 # my %sections = ();
4297 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4298 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4300 # next unless $cust_bill_pkg->pkgnum > 0;
4302 # foreach my $section ( keys %usage_class ) {
4304 # my $usage = $cust_bill_pkg->usage($section);
4306 # next unless $usage && $usage > 0;
4308 # $sections{$section} ||= 0;
4309 # $sections{$section} += $usage;
4315 # map { { 'description' => &{$escape}($_),
4316 # 'subtotal' => $sections{$_},
4317 # 'summarized' => '',
4318 # 'tax_section' => '',
4321 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4325 sub _items_extra_usage_sections {
4327 my $conf = $self->conf;
4335 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4337 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4338 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4339 next unless $cust_bill_pkg->pkgnum > 0;
4341 foreach my $classnum ( keys %usage_class ) {
4342 my $section = $usage_class{$classnum}->classname;
4343 $classnums{$section} = $classnum;
4345 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4346 my $amount = $detail->amount;
4347 next unless $amount && $amount > 0;
4349 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4350 $sections{$section}{amount} += $amount; #subtotal
4351 $sections{$section}{calls}++;
4352 $sections{$section}{duration} += $detail->duration;
4354 my $desc = $detail->regionname;
4355 my $description = $desc;
4356 $description = substr($desc, 0, $maxlength). '...'
4357 if $format eq 'latex' && length($desc) > $maxlength;
4359 $lines{$section}{$desc} ||= {
4360 description => &{$escape}($description),
4361 #pkgpart => $part_pkg->pkgpart,
4362 pkgnum => $cust_bill_pkg->pkgnum,
4367 #unit_amount => $cust_bill_pkg->unitrecur,
4368 quantity => $cust_bill_pkg->quantity,
4369 product_code => 'N/A',
4370 ext_description => [],
4373 $lines{$section}{$desc}{amount} += $amount;
4374 $lines{$section}{$desc}{calls}++;
4375 $lines{$section}{$desc}{duration} += $detail->duration;
4381 my %sectionmap = ();
4382 foreach (keys %sections) {
4383 my $usage_class = $usage_class{$classnums{$_}};
4384 $sectionmap{$_} = { 'description' => &{$escape}($_),
4385 'amount' => $sections{$_}{amount}, #subtotal
4386 'calls' => $sections{$_}{calls},
4387 'duration' => $sections{$_}{duration},
4389 'tax_section' => '',
4390 'sort_weight' => $usage_class->weight,
4391 ( $usage_class->format
4392 ? ( map { $_ => $usage_class->$_($format) }
4393 qw( description_generator header_generator total_generator total_line_generator )
4400 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4404 foreach my $section ( keys %lines ) {
4405 foreach my $line ( keys %{$lines{$section}} ) {
4406 my $l = $lines{$section}{$line};
4407 $l->{section} = $sectionmap{$section};
4408 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4409 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4414 return(\@sections, \@lines);
4420 my $end = $self->_date;
4422 # start at date of previous invoice + 1 second or 0 if no previous invoice
4423 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4424 $start = 0 if !$start;
4427 my $cust_main = $self->cust_main;
4428 my @pkgs = $cust_main->all_pkgs;
4429 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4432 foreach my $pkg ( @pkgs ) {
4433 my @h_cust_svc = $pkg->h_cust_svc($end);
4434 foreach my $h_cust_svc ( @h_cust_svc ) {
4435 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4436 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4438 my $inserted = $h_cust_svc->date_inserted;
4439 my $deleted = $h_cust_svc->date_deleted;
4440 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4442 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4444 # DID either activated or ported in; cannot be both for same DID simultaneously
4445 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4446 && (!$phone_inserted->lnp_status
4447 || $phone_inserted->lnp_status eq ''
4448 || $phone_inserted->lnp_status eq 'native')) {
4451 else { # this one not so clean, should probably move to (h_)svc_phone
4452 my $phone_portedin = qsearchs( 'h_svc_phone',
4453 { 'svcnum' => $h_cust_svc->svcnum,
4454 'lnp_status' => 'portedin' },
4455 FS::h_svc_phone->sql_h_searchs($end),
4457 $num_portedin++ if $phone_portedin;
4460 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4461 if($deleted >= $start && $deleted <= $end && $phone_deleted
4462 && (!$phone_deleted->lnp_status
4463 || $phone_deleted->lnp_status ne 'portingout')) {
4466 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4467 && $phone_deleted->lnp_status
4468 && $phone_deleted->lnp_status eq 'portingout') {
4472 # increment usage minutes
4473 if ( $phone_inserted ) {
4474 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4475 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4478 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4481 # don't look at this service again
4482 push @seen, $h_cust_svc->svcnum;
4486 $minutes = sprintf("%d", $minutes);
4487 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4488 . "$num_deactivated Ported-Out: $num_portedout ",
4489 "Total Minutes: $minutes");
4492 sub _items_accountcode_cdr {
4497 my $section = { 'amount' => 0,
4500 'sort_weight' => '',
4502 'description' => 'Usage by Account Code',
4508 my %accountcodes = ();
4510 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4511 next unless $cust_bill_pkg->pkgnum > 0;
4513 my @header = $cust_bill_pkg->details_header;
4514 next unless scalar(@header);
4515 $section->{'header'} = join(',',@header);
4517 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4519 $section->{'header'} = $detail->formatted('format' => $format)
4520 if($detail->detail eq $section->{'header'});
4522 my $accountcode = $detail->accountcode;
4523 next unless $accountcode;
4525 my $amount = $detail->amount;
4526 next unless $amount && $amount > 0;
4528 $accountcodes{$accountcode} ||= {
4529 description => $accountcode,
4536 product_code => 'N/A',
4537 section => $section,
4538 ext_description => [ $section->{'header'} ],
4542 $section->{'amount'} += $amount;
4543 $accountcodes{$accountcode}{'amount'} += $amount;
4544 $accountcodes{$accountcode}{calls}++;
4545 $accountcodes{$accountcode}{duration} += $detail->duration;
4546 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4550 foreach my $l ( values %accountcodes ) {
4551 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4552 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4553 foreach my $sorted_detail ( @sorted_detail ) {
4554 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4556 delete $l->{detail_temp};
4560 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4562 return ($section,\@sorted_lines);
4565 sub _items_svc_phone_sections {
4567 my $conf = $self->conf;
4575 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4577 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4578 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4580 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4581 next unless $cust_bill_pkg->pkgnum > 0;
4583 my @header = $cust_bill_pkg->details_header;
4584 next unless scalar(@header);
4586 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4588 my $phonenum = $detail->phonenum;
4589 next unless $phonenum;
4591 my $amount = $detail->amount;
4592 next unless $amount && $amount > 0;
4594 $sections{$phonenum} ||= { 'amount' => 0,
4597 'sort_weight' => -1,
4598 'phonenum' => $phonenum,
4600 $sections{$phonenum}{amount} += $amount; #subtotal
4601 $sections{$phonenum}{calls}++;
4602 $sections{$phonenum}{duration} += $detail->duration;
4604 my $desc = $detail->regionname;
4605 my $description = $desc;
4606 $description = substr($desc, 0, $maxlength). '...'
4607 if $format eq 'latex' && length($desc) > $maxlength;
4609 $lines{$phonenum}{$desc} ||= {
4610 description => &{$escape}($description),
4611 #pkgpart => $part_pkg->pkgpart,
4619 product_code => 'N/A',
4620 ext_description => [],
4623 $lines{$phonenum}{$desc}{amount} += $amount;
4624 $lines{$phonenum}{$desc}{calls}++;
4625 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4627 my $line = $usage_class{$detail->classnum}->classname;
4628 $sections{"$phonenum $line"} ||=
4632 'sort_weight' => $usage_class{$detail->classnum}->weight,
4633 'phonenum' => $phonenum,
4634 'header' => [ @header ],
4636 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4637 $sections{"$phonenum $line"}{calls}++;
4638 $sections{"$phonenum $line"}{duration} += $detail->duration;
4640 $lines{"$phonenum $line"}{$desc} ||= {
4641 description => &{$escape}($description),
4642 #pkgpart => $part_pkg->pkgpart,
4650 product_code => 'N/A',
4651 ext_description => [],
4654 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4655 $lines{"$phonenum $line"}{$desc}{calls}++;
4656 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4657 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4658 $detail->formatted('format' => $format);
4663 my %sectionmap = ();
4664 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4665 foreach ( keys %sections ) {
4666 my @header = @{ $sections{$_}{header} || [] };
4668 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4669 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4670 my $usage_class = $summary ? $simple : $usage_simple;
4671 my $ending = $summary ? ' usage charges' : '';
4674 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4676 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4677 'amount' => $sections{$_}{amount}, #subtotal
4678 'calls' => $sections{$_}{calls},
4679 'duration' => $sections{$_}{duration},
4681 'tax_section' => '',
4682 'phonenum' => $sections{$_}{phonenum},
4683 'sort_weight' => $sections{$_}{sort_weight},
4684 'post_total' => $summary, #inspire pagebreak
4686 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4687 qw( description_generator
4690 total_line_generator
4697 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4698 $a->{sort_weight} <=> $b->{sort_weight}
4703 foreach my $section ( keys %lines ) {
4704 foreach my $line ( keys %{$lines{$section}} ) {
4705 my $l = $lines{$section}{$line};
4706 $l->{section} = $sectionmap{$section};
4707 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4708 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4713 if($conf->exists('phone_usage_class_summary')) {
4714 # this only works with Latex
4718 # after this, we'll have only two sections per DID:
4719 # Calls Summary and Calls Detail
4720 foreach my $section ( @sections ) {
4721 if($section->{'post_total'}) {
4722 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4723 $section->{'total_line_generator'} = sub { '' };
4724 $section->{'total_generator'} = sub { '' };
4725 $section->{'header_generator'} = sub { '' };
4726 $section->{'description_generator'} = '';
4727 push @newsections, $section;
4728 my %calls_detail = %$section;
4729 $calls_detail{'post_total'} = '';
4730 $calls_detail{'sort_weight'} = '';
4731 $calls_detail{'description_generator'} = sub { '' };
4732 $calls_detail{'header_generator'} = sub {
4733 return ' & Date/Time & Called Number & Duration & Price'
4734 if $format eq 'latex';
4737 $calls_detail{'description'} = 'Calls Detail: '
4738 . $section->{'phonenum'};
4739 push @newsections, \%calls_detail;
4743 # after this, each usage class is collapsed/summarized into a single
4744 # line under the Calls Summary section
4745 foreach my $newsection ( @newsections ) {
4746 if($newsection->{'post_total'}) { # this means Calls Summary
4747 foreach my $section ( @sections ) {
4748 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4749 && !$section->{'post_total'});
4750 my $newdesc = $section->{'description'};
4751 my $tn = $section->{'phonenum'};
4752 $newdesc =~ s/$tn//g;
4753 my $line = { ext_description => [],
4757 calls => $section->{'calls'},
4758 section => $newsection,
4759 duration => $section->{'duration'},
4760 description => $newdesc,
4761 amount => sprintf("%.2f",$section->{'amount'}),
4762 product_code => 'N/A',
4764 push @newlines, $line;
4769 # after this, Calls Details is populated with all CDRs
4770 foreach my $newsection ( @newsections ) {
4771 if(!$newsection->{'post_total'}) { # this means Calls Details
4772 foreach my $line ( @lines ) {
4773 next unless (scalar(@{$line->{'ext_description'}}) &&
4774 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4776 my @extdesc = @{$line->{'ext_description'}};
4778 foreach my $extdesc ( @extdesc ) {
4779 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4780 push @newextdesc, $extdesc;
4782 $line->{'ext_description'} = \@newextdesc;
4783 $line->{'section'} = $newsection;
4784 push @newlines, $line;
4789 return(\@newsections, \@newlines);
4792 return(\@sections, \@lines);
4796 sub _items { # seems to be unused
4799 #my @display = scalar(@_)
4801 # : qw( _items_previous _items_pkg );
4802 # #: qw( _items_pkg );
4803 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4804 my @display = qw( _items_previous _items_pkg );
4807 foreach my $display ( @display ) {
4808 push @b, $self->$display(@_);
4813 sub _items_previous {
4815 my $conf = $self->conf;
4816 my $cust_main = $self->cust_main;
4817 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4819 foreach ( @pr_cust_bill ) {
4820 my $date = $conf->exists('invoice_show_prior_due_date')
4821 ? 'due '. $_->due_date2str($date_format)
4822 : $self->time2str_local($date_format, $_->_date);
4824 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4825 #'pkgpart' => 'N/A',
4827 'amount' => sprintf("%.2f", $_->owed),
4833 # 'description' => 'Previous Balance',
4834 # #'pkgpart' => 'N/A',
4835 # 'pkgnum' => 'N/A',
4836 # 'amount' => sprintf("%10.2f", $pr_total ),
4837 # 'ext_description' => [ map {
4838 # "Invoice ". $_->invnum.
4839 # " (". time2str("%x",$_->_date). ") ".
4840 # sprintf("%10.2f", $_->owed)
4841 # } @pr_cust_bill ],
4846 =item _items_pkg [ OPTIONS ]
4848 Return line item hashes for each package item on this invoice. Nearly
4851 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4853 The only OPTIONS accepted is 'section', which may point to a hashref
4854 with a key named 'condensed', which may have a true value. If it
4855 does, this method tries to merge identical items into items with
4856 'quantity' equal to the number of items (not the sum of their
4857 separate quantities, for some reason).
4865 warn "$me _items_pkg searching for all package line items\n"
4868 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4870 warn "$me _items_pkg filtering line items\n"
4872 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4874 if ($options{section} && $options{section}->{condensed}) {
4876 warn "$me _items_pkg condensing section\n"
4880 local $Storable::canonical = 1;
4881 foreach ( @items ) {
4883 delete $item->{ref};
4884 delete $item->{ext_description};
4885 my $key = freeze($item);
4886 $itemshash{$key} ||= 0;
4887 $itemshash{$key} ++; # += $item->{quantity};
4889 @items = sort { $a->{description} cmp $b->{description} }
4890 map { my $i = thaw($_);
4891 $i->{quantity} = $itemshash{$_};
4893 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4899 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4906 return 0 unless $a->itemdesc cmp $b->itemdesc;
4907 return -1 if $b->itemdesc eq 'Tax';
4908 return 1 if $a->itemdesc eq 'Tax';
4909 return -1 if $b->itemdesc eq 'Other surcharges';
4910 return 1 if $a->itemdesc eq 'Other surcharges';
4911 $a->itemdesc cmp $b->itemdesc;
4916 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4917 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4920 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4922 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4923 list of hashrefs describing the line items they generate on the invoice.
4925 OPTIONS may include:
4927 format: the invoice format.
4929 escape_function: the function used to escape strings.
4931 DEPRECATED? (expensive, mostly unused?)
4932 format_function: the function used to format CDRs.
4934 section: a hashref containing 'description'; if this is present,
4935 cust_bill_pkg_display records not belonging to this section are
4938 multisection: a flag indicating that this is a multisection invoice,
4939 which does something complicated.
4941 multilocation: a flag to display the location label for the package.
4943 Returns a list of hashrefs, each of which may contain:
4945 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4946 ext_description, which is an arrayref of detail lines to show below
4951 sub _items_cust_bill_pkg {
4953 my $conf = $self->conf;
4954 my $cust_bill_pkgs = shift;
4957 my $format = $opt{format} || '';
4958 my $escape_function = $opt{escape_function} || sub { shift };
4959 my $format_function = $opt{format_function} || '';
4960 my $no_usage = $opt{no_usage} || '';
4961 my $unsquelched = $opt{unsquelched} || ''; #unused
4962 my $section = $opt{section}->{description} if $opt{section};
4963 my $summary_page = $opt{summary_page} || ''; #unused
4964 my $multilocation = $opt{multilocation} || '';
4965 my $multisection = $opt{multisection} || '';
4966 my $discount_show_always = 0;
4968 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4970 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4973 my ($s, $r, $u) = ( undef, undef, undef );
4974 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4977 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4978 if ( $_ && !$cust_bill_pkg->hidden ) {
4979 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4980 $_->{amount} =~ s/^\-0\.00$/0.00/;
4981 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4983 if $_->{amount} != 0
4984 || $discount_show_always
4985 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4986 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4992 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4994 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4995 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4998 foreach my $display ( grep { defined($section)
4999 ? $_->section eq $section
5002 #grep { !$_->summary || !$summary_page } # bunk!
5003 grep { !$_->summary || $multisection }
5004 @cust_bill_pkg_display
5008 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5009 $display->billpkgdisplaynum. "\n"
5012 my $type = $display->type;
5014 my $desc = $cust_bill_pkg->desc;
5015 $desc = substr($desc, 0, $maxlength). '...'
5016 if $format eq 'latex' && length($desc) > $maxlength;
5018 my %details_opt = ( 'format' => $format,
5019 'escape_function' => $escape_function,
5020 'format_function' => $format_function,
5021 'no_usage' => $opt{'no_usage'},
5024 if ( $cust_bill_pkg->pkgnum > 0 ) {
5026 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5029 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5031 # which pkgpart to show for display purposes?
5032 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5034 # start/end dates for invoice formats that do nonstandard
5036 my %item_dates = ();
5037 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5038 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5040 if ( (!$type || $type eq 'S')
5041 && ( $cust_bill_pkg->setup != 0
5042 || $cust_bill_pkg->setup_show_zero
5047 warn "$me _items_cust_bill_pkg adding setup\n"
5050 my $description = $desc;
5051 $description .= ' Setup'
5052 if $cust_bill_pkg->recur != 0
5053 || $discount_show_always
5054 || $cust_bill_pkg->recur_show_zero;
5058 unless ( $cust_pkg->part_pkg->hide_svc_detail
5059 || $cust_bill_pkg->hidden )
5062 my @svc_labels = map &{$escape_function}($_),
5063 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5064 push @d, @svc_labels
5065 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5066 $svc_label = $svc_labels[0];
5068 if ( $multilocation ) {
5069 my $loc = $cust_pkg->location_label;
5070 $loc = substr($loc, 0, $maxlength). '...'
5071 if $format eq 'latex' && length($loc) > $maxlength;
5072 push @d, &{$escape_function}($loc);
5075 } #unless hiding service details
5077 push @d, $cust_bill_pkg->details(%details_opt)
5078 if $cust_bill_pkg->recur == 0;
5080 if ( $cust_bill_pkg->hidden ) {
5081 $s->{amount} += $cust_bill_pkg->setup;
5082 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5083 push @{ $s->{ext_description} }, @d;
5087 description => $description,
5088 pkgpart => $pkgpart,
5089 pkgnum => $cust_bill_pkg->pkgnum,
5090 amount => $cust_bill_pkg->setup,
5091 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5092 unit_amount => $cust_bill_pkg->unitsetup,
5093 quantity => $cust_bill_pkg->quantity,
5094 ext_description => \@d,
5095 svc_label => ($svc_label || ''),
5101 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5103 $cust_bill_pkg->recur != 0
5104 || $cust_bill_pkg->setup == 0
5105 || $discount_show_always
5106 || $cust_bill_pkg->recur_show_zero
5111 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5114 my $is_summary = $display->summary;
5115 my $description = ($is_summary && $type && $type eq 'U')
5116 ? "Usage charges" : $desc;
5118 my $part_pkg = $cust_pkg->part_pkg;
5120 #pry be a bit more efficient to look some of this conf stuff up
5123 $conf->exists('disable_line_item_date_ranges')
5124 || $part_pkg->option('disable_line_item_date_ranges',1)
5125 || ! $cust_bill_pkg->sdate
5126 || ! $cust_bill_pkg->edate
5129 my $date_style = '';
5130 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5131 $cust_main->agentnum
5133 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5134 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5135 $cust_main->agentnum
5137 if ( defined($date_style) && $date_style eq 'month_of' ) {
5138 $time_period = $self->time2str_local('The month of %B', $cust_bill_pkg->sdate);
5139 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5140 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5141 $cust_main->agentnum
5143 $desc .= ' ' unless $desc =~ /\s$/;
5144 $time_period = $desc. $self->time2str_local('%B', $cust_bill_pkg->sdate);
5146 $time_period = $self->time2str_local($date_format, $cust_bill_pkg->sdate).
5147 " - ". $self->time2str_local($date_format, $cust_bill_pkg->edate);
5149 $description .= " ($time_period)";
5153 my @seconds = (); # for display of usage info
5156 #at least until cust_bill_pkg has "past" ranges in addition to
5157 #the "future" sdate/edate ones... see #3032
5158 my @dates = ( $self->_date );
5159 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5160 push @dates, $prev->sdate if $prev;
5161 push @dates, undef if !$prev;
5163 unless ( $cust_pkg->part_pkg->hide_svc_detail
5164 || $cust_bill_pkg->itemdesc
5165 || $cust_bill_pkg->hidden
5166 || $is_summary && $type && $type eq 'U' )
5169 warn "$me _items_cust_bill_pkg adding service details\n"
5172 my @svc_labels = map &{$escape_function}($_),
5173 $cust_pkg->h_labels_short(@dates, 'I');
5174 push @d, @svc_labels
5175 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5176 $svc_label = $svc_labels[0];
5178 warn "$me _items_cust_bill_pkg done adding service details\n"
5181 if ( $multilocation ) {
5182 my $loc = $cust_pkg->location_label;
5183 $loc = substr($loc, 0, $maxlength). '...'
5184 if $format eq 'latex' && length($loc) > $maxlength;
5185 push @d, &{$escape_function}($loc);
5188 # Display of seconds_since_sqlradacct:
5189 # On the invoice, when processing @detail_items, look for a field
5190 # named 'seconds'. This will contain total seconds for each
5191 # service, in the same order as @ext_description. For services
5192 # that don't support this it will show undef.
5193 if ( $conf->exists('svc_acct-usage_seconds')
5194 and ! $cust_bill_pkg->pkgpart_override ) {
5195 foreach my $cust_svc (
5196 $cust_pkg->h_cust_svc(@dates, 'I')
5199 # eval because not having any part_export_usage exports
5200 # is a fatal error, last_bill/_date because that's how
5201 # sqlradius_hour billing does it
5203 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5205 push @seconds, $sec;
5207 } #if svc_acct-usage_seconds
5211 unless ( $is_summary ) {
5212 warn "$me _items_cust_bill_pkg adding details\n"
5215 #instead of omitting details entirely in this case (unwanted side
5216 # effects), just omit CDRs
5217 $details_opt{'no_usage'} = 1
5218 if $type && $type eq 'R';
5220 push @d, $cust_bill_pkg->details(%details_opt);
5223 warn "$me _items_cust_bill_pkg calculating amount\n"
5228 $amount = $cust_bill_pkg->recur;
5229 } elsif ($type eq 'R') {
5230 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5231 } elsif ($type eq 'U') {
5232 $amount = $cust_bill_pkg->usage;
5235 if ( !$type || $type eq 'R' ) {
5237 warn "$me _items_cust_bill_pkg adding recur\n"
5240 if ( $cust_bill_pkg->hidden ) {
5241 $r->{amount} += $amount;
5242 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5243 push @{ $r->{ext_description} }, @d;
5246 description => $description,
5247 pkgpart => $pkgpart,
5248 pkgnum => $cust_bill_pkg->pkgnum,
5250 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5251 unit_amount => $cust_bill_pkg->unitrecur,
5252 quantity => $cust_bill_pkg->quantity,
5254 ext_description => \@d,
5255 svc_label => ($svc_label || ''),
5257 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5260 } else { # $type eq 'U'
5262 warn "$me _items_cust_bill_pkg adding usage\n"
5265 if ( $cust_bill_pkg->hidden ) {
5266 $u->{amount} += $amount;
5267 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5268 push @{ $u->{ext_description} }, @d;
5271 description => $description,
5272 pkgpart => $pkgpart,
5273 pkgnum => $cust_bill_pkg->pkgnum,
5275 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5276 unit_amount => $cust_bill_pkg->unitrecur,
5277 quantity => $cust_bill_pkg->quantity,
5279 ext_description => \@d,
5284 } # recurring or usage with recurring charge
5286 } else { #pkgnum tax or one-shot line item (??)
5288 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5291 if ( $cust_bill_pkg->setup != 0 ) {
5293 'description' => $desc,
5294 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5297 if ( $cust_bill_pkg->recur != 0 ) {
5299 'description' => "$desc (".
5300 $self->time2str_local($date_format, $cust_bill_pkg->sdate). ' - '.
5301 $self->time2str_local($date_format, $cust_bill_pkg->edate). ')',
5302 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5310 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5311 && $conf->exists('discount-show-always'));
5315 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5317 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5318 $_->{amount} =~ s/^\-0\.00$/0.00/;
5319 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5321 if $_->{amount} != 0
5322 || $discount_show_always
5323 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5324 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5328 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5335 sub _items_credits {
5336 my( $self, %opt ) = @_;
5337 my $trim_len = $opt{'trim_len'} || 60;
5342 if ( $self->conf->exists('previous_balance-payments_since') ) {
5343 if ( $opt{'template'} eq 'statement' ) {
5344 # then the current bill is a "statement" (i.e. an invoice sent as
5345 # a payment receipt)
5346 # and in that case we want to see payments on or after THIS invoice
5347 @objects = qsearch('cust_credit', {
5348 'custnum' => $self->custnum,
5349 '_date' => {op => '>=', value => $self->_date},
5353 $date = $self->previous_bill->_date if $self->previous_bill;
5354 @objects = qsearch('cust_credit', {
5355 'custnum' => $self->custnum,
5356 '_date' => {op => '>=', value => $date},
5360 @objects = $self->cust_credited;
5363 foreach my $obj ( @objects ) {
5364 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5366 my $reason = substr($cust_credit->reason, 0, $trim_len);
5367 $reason .= '...' if length($reason) < length($cust_credit->reason);
5368 $reason = " ($reason) " if $reason;
5371 #'description' => 'Credit ref\#'. $_->crednum.
5372 # " (". time2str("%x",$_->cust_credit->_date) .")".
5374 'description' => $self->mt('Credit applied').' '.
5375 $self->time2str_local($date_format,$obj->_date). $reason,
5376 'amount' => sprintf("%.2f",$obj->amount),
5384 sub _items_payments {
5389 my $detailed = $self->conf->exists('invoice_payment_details');
5391 if ( $self->conf->exists('previous_balance-payments_since') ) {
5392 # then show payments dated on/after the previous bill...
5393 if ( $opt{'template'} eq 'statement' ) {
5394 # then the current bill is a "statement" (i.e. an invoice sent as
5395 # a payment receipt)
5396 # and in that case we want to see payments on or after THIS invoice
5397 @objects = qsearch('cust_pay', {
5398 'custnum' => $self->custnum,
5399 '_date' => {op => '>=', value => $self->_date},
5402 # the normal case: payments on or after the previous invoice
5404 $date = $self->previous_bill->_date if $self->previous_bill;
5405 @objects = qsearch('cust_pay', {
5406 'custnum' => $self->custnum,
5407 '_date' => {op => '>=', value => $date},
5409 # and before the current bill...
5410 @objects = grep { $_->_date < $self->_date } @objects;
5413 @objects = $self->cust_bill_pay;
5416 foreach my $obj (@objects) {
5417 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5418 my $desc = $self->mt('Payment received').' '.
5419 $self->time2str_local($date_format, $cust_pay->_date );
5420 $desc .= $self->mt(' via ') .
5421 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5425 'description' => $desc,
5426 'amount' => sprintf("%.2f", $obj->amount )
5434 =item _items_discounts_avail
5436 Returns an array of line item hashrefs representing available term discounts
5437 for this invoice. This makes the same assumptions that apply to term
5438 discounts in general: that the package is billed monthly, at a flat rate,
5439 with no usage charges. A prorated first month will be handled, as will
5440 a setup fee if the discount is allowed to apply to setup fees.
5444 sub _items_discounts_avail {
5446 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5448 my %plans = $self->discount_plans;
5450 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5454 my $plan = $plans{$months};
5456 my $term_total = sprintf('%.2f', $plan->discounted_total);
5457 my $percent = sprintf('%.0f',
5458 100 * (1 - $term_total / $plan->base_total) );
5459 my $permonth = sprintf('%.2f', $term_total / $months);
5460 my $detail = $self->mt('discount on item'). ' '.
5461 join(', ', map { "#$_" } $plan->pkgnums)
5464 # discounts for non-integer months don't work anyway
5465 $months = sprintf("%d", $months);
5468 description => $self->mt('Save [_1]% by paying for [_2] months',
5470 amount => $self->mt('[_1] ([_2] per month)',
5471 $term_total, $money_char.$permonth),
5472 ext_description => ($detail || ''),
5475 sort { $b <=> $a } keys %plans;
5479 =item call_details [ OPTION => VALUE ... ]
5481 Returns an array of CSV strings representing the call details for this invoice
5482 The only option available is the boolean prepend_billed_number
5487 my ($self, %opt) = @_;
5489 my $format_function = sub { shift };
5491 if ($opt{prepend_billed_number}) {
5492 $format_function = sub {
5496 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5501 my @details = map { $_->details( 'format_function' => $format_function,
5502 'escape_function' => sub{ return() },
5506 $self->cust_bill_pkg;
5507 my $header = $details[0];
5508 ( $header, grep { $_ ne $header } @details );
5518 =item process_reprint
5522 sub process_reprint {
5523 process_re_X('print', @_);
5526 =item process_reemail
5530 sub process_reemail {
5531 process_re_X('email', @_);
5539 process_re_X('fax', @_);
5547 process_re_X('ftp', @_);
5554 sub process_respool {
5555 process_re_X('spool', @_);
5558 use Storable qw(thaw);
5562 my( $method, $job ) = ( shift, shift );
5563 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5565 my $param = thaw(decode_base64(shift));
5566 warn Dumper($param) if $DEBUG;
5577 my($method, $job, %param ) = @_;
5579 warn "re_X $method for job $job with param:\n".
5580 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5583 #some false laziness w/search/cust_bill.html
5585 my $orderby = 'ORDER BY cust_bill._date';
5587 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5589 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5591 my @cust_bill = qsearch( {
5592 #'select' => "cust_bill.*",
5593 'table' => 'cust_bill',
5594 'addl_from' => $addl_from,
5596 'extra_sql' => $extra_sql,
5597 'order_by' => $orderby,
5601 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5603 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5606 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5607 foreach my $cust_bill ( @cust_bill ) {
5608 $cust_bill->$method();
5610 if ( $job ) { #progressbar foo
5612 if ( time - $min_sec > $last ) {
5613 my $error = $job->update_statustext(
5614 int( 100 * $num / scalar(@cust_bill) )
5616 die $error if $error;
5627 =head1 CLASS METHODS
5633 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5638 my ($class, $start, $end) = @_;
5640 $class->paid_sql($start, $end). ' - '.
5641 $class->credited_sql($start, $end);
5646 Returns an SQL fragment to retreive the net amount (charged minus credited).
5651 my ($class, $start, $end) = @_;
5652 'charged - '. $class->credited_sql($start, $end);
5657 Returns an SQL fragment to retreive the amount paid against this invoice.
5662 my ($class, $start, $end) = @_;
5663 $start &&= "AND cust_bill_pay._date <= $start";
5664 $end &&= "AND cust_bill_pay._date > $end";
5665 $start = '' unless defined($start);
5666 $end = '' unless defined($end);
5667 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5668 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5673 Returns an SQL fragment to retreive the amount credited against this invoice.
5678 my ($class, $start, $end) = @_;
5679 $start &&= "AND cust_credit_bill._date <= $start";
5680 $end &&= "AND cust_credit_bill._date > $end";
5681 $start = '' unless defined($start);
5682 $end = '' unless defined($end);
5683 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5684 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5689 Returns an SQL fragment to retrieve the due date of an invoice.
5690 Currently only supported on PostgreSQL.
5695 my $conf = new FS::Conf;
5699 cust_bill.invoice_terms,
5700 cust_main.invoice_terms,
5701 \''.($conf->config('invoice_default_terms') || '').'\'
5702 ), E\'Net (\\\\d+)\'
5704 ) * 86400 + cust_bill._date'
5707 =item search_sql_where HASHREF
5709 Class method which returns an SQL WHERE fragment to search for parameters
5710 specified in HASHREF. Valid parameters are
5716 List reference of start date, end date, as UNIX timestamps.
5726 List reference of charged limits (exclusive).
5730 List reference of charged limits (exclusive).
5734 flag, return open invoices only
5738 flag, return net invoices only
5742 =item newest_percust
5746 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5750 sub search_sql_where {
5751 my($class, $param) = @_;
5753 warn "$me search_sql_where called with params: \n".
5754 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5760 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5761 push @search, "cust_main.agentnum = $1";
5765 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5766 push @search, "cust_main.refnum = $1";
5770 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5771 push @search, "cust_bill.custnum = $1";
5775 if ( $param->{'cust_classnum'} ) {
5776 my $classnums = $param->{'cust_classnum'};
5777 $classnums = [ $classnums ] if !ref($classnums);
5778 $classnums = [ grep /^\d+$/, @$classnums ];
5779 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5784 if ( $param->{_date} ) {
5785 my($beginning, $ending) = @{$param->{_date}};
5787 push @search, "cust_bill._date >= $beginning",
5788 "cust_bill._date < $ending";
5792 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5793 push @search, "cust_bill.invnum >= $1";
5795 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5796 push @search, "cust_bill.invnum <= $1";
5800 if ( $param->{charged} ) {
5801 my @charged = ref($param->{charged})
5802 ? @{ $param->{charged} }
5803 : ($param->{charged});
5805 push @search, map { s/^charged/cust_bill.charged/; $_; }
5809 my $owed_sql = FS::cust_bill->owed_sql;
5812 if ( $param->{owed} ) {
5813 my @owed = ref($param->{owed})
5814 ? @{ $param->{owed} }
5816 push @search, map { s/^owed/$owed_sql/; $_; }
5821 push @search, "0 != $owed_sql"
5822 if $param->{'open'};
5823 push @search, '0 != '. FS::cust_bill->net_sql
5827 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5828 if $param->{'days'};
5831 if ( $param->{'newest_percust'} ) {
5833 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5834 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5836 my @newest_where = map { my $x = $_;
5837 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5840 grep ! /^cust_main./, @search;
5841 my $newest_where = scalar(@newest_where)
5842 ? ' AND '. join(' AND ', @newest_where)
5846 push @search, "cust_bill._date = (
5847 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5848 WHERE newest_cust_bill.custnum = cust_bill.custnum
5854 #promised_date - also has an option to accept nulls
5855 if ( $param->{promised_date} ) {
5856 my($beginning, $ending, $null) = @{$param->{promised_date}};
5858 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5859 "cust_bill.promised_date < $ending )" .
5860 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5863 #agent virtualization
5864 my $curuser = $FS::CurrentUser::CurrentUser;
5865 if ( $curuser->username eq 'fs_queue'
5866 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5868 my $newuser = qsearchs('access_user', {
5869 'username' => $username,
5873 $curuser = $newuser;
5875 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5878 push @search, $curuser->agentnums_sql;
5880 join(' AND ', @search );
5892 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5893 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base