4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
147 =item promised_date - customer promised payment date, for collection
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
165 sub cust_linked { $_[0]->cust_main_custnum; }
166 sub cust_unlinked_msg {
168 "WARNING: can't find cust_main.custnum ". $self->custnum.
169 ' (cust_bill.invnum '. $self->invnum. ')';
174 Adds this invoice to the database ("Posts" the invoice). If there is an error,
175 returns the error, otherwise returns false.
181 warn "$me insert called\n" if $DEBUG;
183 local $SIG{HUP} = 'IGNORE';
184 local $SIG{INT} = 'IGNORE';
185 local $SIG{QUIT} = 'IGNORE';
186 local $SIG{TERM} = 'IGNORE';
187 local $SIG{TSTP} = 'IGNORE';
188 local $SIG{PIPE} = 'IGNORE';
190 my $oldAutoCommit = $FS::UID::AutoCommit;
191 local $FS::UID::AutoCommit = 0;
194 my $error = $self->SUPER::insert;
196 $dbh->rollback if $oldAutoCommit;
200 if ( $self->get('cust_bill_pkg') ) {
201 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202 $cust_bill_pkg->invnum($self->invnum);
203 my $error = $cust_bill_pkg->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "can't create invoice line item: $error";
211 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 This method now works but you probably shouldn't use it. Instead, apply a
219 credit against the invoice.
221 Using this method to delete invoices outright is really, really bad. There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
225 Really, don't use it.
231 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
256 foreach my $linked ( $self->$table() ) {
257 my $error = $linked->delete;
259 $dbh->rollback if $oldAutoCommit;
266 my $error = $self->SUPER::delete(@_);
268 $dbh->rollback if $oldAutoCommit;
272 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 =item replace [ OLD_RECORD ]
280 You can, but probably shouldn't modify invoices...
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record. If there is an error, returns the error,
284 otherwise returns false.
288 #replace can be inherited from Record.pm
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
294 my( $new, $old ) = ( shift, shift );
295 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296 #return "Can't change _date!" unless $old->_date eq $new->_date;
297 return "Can't change _date" unless $old->_date == $new->_date;
298 return "Can't change charged" unless $old->charged == $new->charged
299 || $old->charged == 0
300 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
306 =item add_cc_surcharge
312 sub add_cc_surcharge {
313 my ($self, $pkgnum, $amount) = (shift, shift, shift);
316 my $cust_bill_pkg = new FS::cust_bill_pkg({
317 'invnum' => $self->invnum,
321 $error = $cust_bill_pkg->insert;
322 return $error if $error;
324 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325 $self->charged($self->charged+$amount);
326 $error = $self->replace;
327 return $error if $error;
329 $self->apply_payments_and_credits;
335 Checks all fields to make sure this is a valid invoice. If there is an error,
336 returns the error, otherwise returns false. Called by the insert and replace
345 $self->ut_numbern('invnum')
346 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347 || $self->ut_numbern('_date')
348 || $self->ut_money('charged')
349 || $self->ut_numbern('printed')
350 || $self->ut_enum('closed', [ '', 'Y' ])
351 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352 || $self->ut_numbern('agent_invid') #varchar?
354 return $error if $error;
356 $self->_date(time) unless $self->_date;
358 $self->printed(0) if $self->printed eq '';
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
372 my $conf = $self->conf;
373 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374 return $self->agent_invid;
376 return $self->invnum;
382 Returns the customer's last invoice before this one.
388 if ( !$self->get('previous_bill') ) {
389 $self->set('previous_bill', qsearchs({
390 'table' => 'cust_bill',
391 'hashref' => { 'custnum' => $self->custnum,
392 '_date' => { op=>'<', value=>$self->_date } },
393 'order_by' => 'ORDER BY _date DESC LIMIT 1',
396 $self->get('previous_bill');
401 Returns a list consisting of the total previous balance for this customer,
402 followed by the previous outstanding invoices (as FS::cust_bill objects also).
409 my @cust_bill = sort { $a->_date <=> $b->_date }
410 grep { $_->owed != 0 }
411 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
412 #'_date' => { op=>'<', value=>$self->_date },
413 'invnum' => { op=>'<', value=>$self->invnum },
416 foreach ( @cust_bill ) { $total += $_->owed; }
420 =item enable_previous
422 Whether to show the 'Previous Charges' section when printing this invoice.
423 The negation of the 'disable_previous_balance' config setting.
427 sub enable_previous {
429 my $agentnum = $self->cust_main->agentnum;
430 !$self->conf->exists('disable_previous_balance', $agentnum);
435 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
442 { 'table' => 'cust_bill_pkg',
443 'hashref' => { 'invnum' => $self->invnum },
444 'order_by' => 'ORDER BY billpkgnum',
449 =item cust_bill_pkg_pkgnum PKGNUM
451 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
456 sub cust_bill_pkg_pkgnum {
457 my( $self, $pkgnum ) = @_;
459 { 'table' => 'cust_bill_pkg',
460 'hashref' => { 'invnum' => $self->invnum,
463 'order_by' => 'ORDER BY billpkgnum',
470 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
477 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
478 $self->cust_bill_pkg;
480 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
485 Returns true if any of the packages (or their definitions) corresponding to the
486 line items for this invoice have the no_auto flag set.
492 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
495 =item open_cust_bill_pkg
497 Returns the open line items for this invoice.
499 Note that cust_bill_pkg with both setup and recur fees are returned as two
500 separate line items, each with only one fee.
504 # modeled after cust_main::open_cust_bill
505 sub open_cust_bill_pkg {
508 # grep { $_->owed > 0 } $self->cust_bill_pkg
510 my %other = ( 'recur' => 'setup',
511 'setup' => 'recur', );
513 foreach my $field ( qw( recur setup )) {
514 push @open, map { $_->set( $other{$field}, 0 ); $_; }
515 grep { $_->owed($field) > 0 }
516 $self->cust_bill_pkg;
522 =item cust_bill_event
524 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
528 sub cust_bill_event {
530 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
533 =item num_cust_bill_event
535 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
539 sub num_cust_bill_event {
542 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
543 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
544 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
545 $sth->fetchrow_arrayref->[0];
550 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
554 #false laziness w/cust_pkg.pm
558 'table' => 'cust_event',
559 'addl_from' => 'JOIN part_event USING ( eventpart )',
560 'hashref' => { 'tablenum' => $self->invnum },
561 'extra_sql' => " AND eventtable = 'cust_bill' ",
567 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
571 #false laziness w/cust_pkg.pm
575 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
576 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
577 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
578 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
579 $sth->fetchrow_arrayref->[0];
584 Returns the customer (see L<FS::cust_main>) for this invoice.
590 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
593 =item cust_suspend_if_balance_over AMOUNT
595 Suspends the customer associated with this invoice if the total amount owed on
596 this invoice and all older invoices is greater than the specified amount.
598 Returns a list: an empty list on success or a list of errors.
602 sub cust_suspend_if_balance_over {
603 my( $self, $amount ) = ( shift, shift );
604 my $cust_main = $self->cust_main;
605 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
608 $cust_main->suspend(@_);
614 Depreciated. See the cust_credited method.
616 #Returns a list consisting of the total previous credited (see
617 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
618 #outstanding credits (FS::cust_credit objects).
624 croak "FS::cust_bill->cust_credit depreciated; see ".
625 "FS::cust_bill->cust_credit_bill";
628 #my @cust_credit = sort { $a->_date <=> $b->_date }
629 # grep { $_->credited != 0 && $_->_date < $self->_date }
630 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
632 #foreach (@cust_credit) { $total += $_->credited; }
633 #$total, @cust_credit;
638 Depreciated. See the cust_bill_pay method.
640 #Returns all payments (see L<FS::cust_pay>) for this invoice.
646 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
648 #sort { $a->_date <=> $b->_date }
649 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
655 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
658 sub cust_bill_pay_batch {
660 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
665 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
671 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
672 sort { $a->_date <=> $b->_date }
673 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
678 =item cust_credit_bill
680 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
686 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
687 sort { $a->_date <=> $b->_date }
688 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
692 sub cust_credit_bill {
693 shift->cust_credited(@_);
696 #=item cust_bill_pay_pkgnum PKGNUM
698 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
699 #with matching pkgnum.
703 #sub cust_bill_pay_pkgnum {
704 # my( $self, $pkgnum ) = @_;
705 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
706 # sort { $a->_date <=> $b->_date }
707 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
708 # 'pkgnum' => $pkgnum,
713 =item cust_bill_pay_pkg PKGNUM
715 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
716 applied against the matching pkgnum.
720 sub cust_bill_pay_pkg {
721 my( $self, $pkgnum ) = @_;
724 'select' => 'cust_bill_pay_pkg.*',
725 'table' => 'cust_bill_pay_pkg',
726 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
727 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
728 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
729 " AND cust_bill_pkg.pkgnum = $pkgnum",
734 #=item cust_credited_pkgnum PKGNUM
736 #=item cust_credit_bill_pkgnum PKGNUM
738 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
739 #with matching pkgnum.
743 #sub cust_credited_pkgnum {
744 # my( $self, $pkgnum ) = @_;
745 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
746 # sort { $a->_date <=> $b->_date }
747 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
748 # 'pkgnum' => $pkgnum,
753 #sub cust_credit_bill_pkgnum {
754 # shift->cust_credited_pkgnum(@_);
757 =item cust_credit_bill_pkg PKGNUM
759 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
760 applied against the matching pkgnum.
764 sub cust_credit_bill_pkg {
765 my( $self, $pkgnum ) = @_;
768 'select' => 'cust_credit_bill_pkg.*',
769 'table' => 'cust_credit_bill_pkg',
770 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
771 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
772 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
773 " AND cust_bill_pkg.pkgnum = $pkgnum",
778 =item cust_bill_batch
780 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
784 sub cust_bill_batch {
786 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
791 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
792 hash keyed by term length.
798 FS::discount_plan->all($self);
803 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
810 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
812 foreach (@taxlines) { $total += $_->setup; }
818 Returns the amount owed (still outstanding) on this invoice, which is charged
819 minus all payment applications (see L<FS::cust_bill_pay>) and credit
820 applications (see L<FS::cust_credit_bill>).
826 my $balance = $self->charged;
827 $balance -= $_->amount foreach ( $self->cust_bill_pay );
828 $balance -= $_->amount foreach ( $self->cust_credited );
829 $balance = sprintf( "%.2f", $balance);
830 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
835 my( $self, $pkgnum ) = @_;
837 #my $balance = $self->charged;
839 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
841 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
842 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
844 $balance = sprintf( "%.2f", $balance);
845 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
851 Returns true if this invoice should be hidden. See the
852 selfservice-hide_invoices-taxclass configuraiton setting.
858 my $conf = $self->conf;
859 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
861 my @cust_bill_pkg = $self->cust_bill_pkg;
862 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
863 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
866 =item apply_payments_and_credits [ OPTION => VALUE ... ]
868 Applies unapplied payments and credits to this invoice.
870 A hash of optional arguments may be passed. Currently "manual" is supported.
871 If true, a payment receipt is sent instead of a statement when
872 'payment_receipt_email' configuration option is set.
874 If there is an error, returns the error, otherwise returns false.
878 sub apply_payments_and_credits {
879 my( $self, %options ) = @_;
880 my $conf = $self->conf;
882 local $SIG{HUP} = 'IGNORE';
883 local $SIG{INT} = 'IGNORE';
884 local $SIG{QUIT} = 'IGNORE';
885 local $SIG{TERM} = 'IGNORE';
886 local $SIG{TSTP} = 'IGNORE';
887 local $SIG{PIPE} = 'IGNORE';
889 my $oldAutoCommit = $FS::UID::AutoCommit;
890 local $FS::UID::AutoCommit = 0;
893 $self->select_for_update; #mutex
895 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
896 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
898 if ( $conf->exists('pkg-balances') ) {
899 # limit @payments & @credits to those w/ a pkgnum grepped from $self
900 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
901 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
902 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
905 while ( $self->owed > 0 and ( @payments || @credits ) ) {
908 if ( @payments && @credits ) {
910 #decide which goes first by weight of top (unapplied) line item
912 my @open_lineitems = $self->open_cust_bill_pkg;
915 max( map { $_->part_pkg->pay_weight || 0 }
920 my $max_credit_weight =
921 max( map { $_->part_pkg->credit_weight || 0 }
927 #if both are the same... payments first? it has to be something
928 if ( $max_pay_weight >= $max_credit_weight ) {
934 } elsif ( @payments ) {
936 } elsif ( @credits ) {
939 die "guru meditation #12 and 35";
943 if ( $app eq 'pay' ) {
945 my $payment = shift @payments;
946 $unapp_amount = $payment->unapplied;
947 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
948 $app->pkgnum( $payment->pkgnum )
949 if $conf->exists('pkg-balances') && $payment->pkgnum;
951 } elsif ( $app eq 'credit' ) {
953 my $credit = shift @credits;
954 $unapp_amount = $credit->credited;
955 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
956 $app->pkgnum( $credit->pkgnum )
957 if $conf->exists('pkg-balances') && $credit->pkgnum;
960 die "guru meditation #12 and 35";
964 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
965 warn "owed_pkgnum ". $app->pkgnum;
966 $owed = $self->owed_pkgnum($app->pkgnum);
970 next unless $owed > 0;
972 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
973 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
975 $app->invnum( $self->invnum );
977 my $error = $app->insert(%options);
979 $dbh->rollback if $oldAutoCommit;
980 return "Error inserting ". $app->table. " record: $error";
982 die $error if $error;
986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
991 =item generate_email OPTION => VALUE ...
999 sender address, required
1003 alternate template name, optional
1007 text attachment arrayref, optional
1011 email subject, optional
1015 notice name instead of "Invoice", optional
1019 Returns an argument list to be passed to L<FS::Misc::send_email>.
1025 sub generate_email {
1029 my $conf = $self->conf;
1031 my $me = '[FS::cust_bill::generate_email]';
1034 'from' => $args{'from'},
1035 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1039 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1040 'template' => $args{'template'},
1041 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1042 'no_coupon' => $args{'no_coupon'},
1045 my $cust_main = $self->cust_main;
1047 if (ref($args{'to'}) eq 'ARRAY') {
1048 $return{'to'} = $args{'to'};
1050 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1051 $cust_main->invoicing_list
1055 if ( $conf->exists('invoice_html') ) {
1057 warn "$me creating HTML/text multipart message"
1060 $return{'nobody'} = 1;
1062 my $alternative = build MIME::Entity
1063 'Type' => 'multipart/alternative',
1064 #'Encoding' => '7bit',
1065 'Disposition' => 'inline'
1069 if ( $conf->exists('invoice_email_pdf')
1070 and scalar($conf->config('invoice_email_pdf_note')) ) {
1072 warn "$me using 'invoice_email_pdf_note' in multipart message"
1074 $data = [ map { $_ . "\n" }
1075 $conf->config('invoice_email_pdf_note')
1080 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1082 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1083 $data = $args{'print_text'};
1085 $data = [ $self->print_text(\%opt) ];
1090 $alternative->attach(
1091 'Type' => 'text/plain',
1092 'Encoding' => 'quoted-printable',
1093 #'Encoding' => '7bit',
1095 'Disposition' => 'inline',
1102 if ( $conf->exists('invoice_email_pdf')
1103 and scalar($conf->config('invoice_email_pdf_note')) ) {
1105 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1109 $args{'from'} =~ /\@([\w\.\-]+)/;
1110 my $from = $1 || 'example.com';
1111 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1114 my $agentnum = $cust_main->agentnum;
1115 if ( defined($args{'template'}) && length($args{'template'})
1116 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1119 $logo = 'logo_'. $args{'template'}. '.png';
1123 my $image_data = $conf->config_binary( $logo, $agentnum);
1125 $image = build MIME::Entity
1126 'Type' => 'image/png',
1127 'Encoding' => 'base64',
1128 'Data' => $image_data,
1129 'Filename' => 'logo.png',
1130 'Content-ID' => "<$content_id>",
1133 if ($conf->exists('invoice-barcode')) {
1134 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1135 $barcode = build MIME::Entity
1136 'Type' => 'image/png',
1137 'Encoding' => 'base64',
1138 'Data' => $self->invoice_barcode(0),
1139 'Filename' => 'barcode.png',
1140 'Content-ID' => "<$barcode_content_id>",
1142 $opt{'barcode_cid'} = $barcode_content_id;
1145 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1148 $alternative->attach(
1149 'Type' => 'text/html',
1150 'Encoding' => 'quoted-printable',
1151 'Data' => [ '<html>',
1154 ' '. encode_entities($return{'subject'}),
1157 ' <body bgcolor="#e8e8e8">',
1162 'Disposition' => 'inline',
1163 #'Filename' => 'invoice.pdf',
1167 my @otherparts = ();
1168 if ( $cust_main->email_csv_cdr ) {
1170 push @otherparts, build MIME::Entity
1171 'Type' => 'text/csv',
1172 'Encoding' => '7bit',
1173 'Data' => [ map { "$_\n" }
1174 $self->call_details('prepend_billed_number' => 1)
1176 'Disposition' => 'attachment',
1177 'Filename' => 'usage-'. $self->invnum. '.csv',
1182 if ( $conf->exists('invoice_email_pdf') ) {
1187 # multipart/alternative
1193 my $related = build MIME::Entity 'Type' => 'multipart/related',
1194 'Encoding' => '7bit';
1196 #false laziness w/Misc::send_email
1197 $related->head->replace('Content-type',
1198 $related->mime_type.
1199 '; boundary="'. $related->head->multipart_boundary. '"'.
1200 '; type=multipart/alternative'
1203 $related->add_part($alternative);
1205 $related->add_part($image) if $image;
1207 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1209 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1213 #no other attachment:
1215 # multipart/alternative
1220 $return{'content-type'} = 'multipart/related';
1221 if ($conf->exists('invoice-barcode') && $barcode) {
1222 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1224 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1226 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1227 #$return{'disposition'} = 'inline';
1233 if ( $conf->exists('invoice_email_pdf') ) {
1234 warn "$me creating PDF attachment"
1237 #mime parts arguments a la MIME::Entity->build().
1238 $return{'mimeparts'} = [
1239 { $self->mimebuild_pdf(\%opt) }
1243 if ( $conf->exists('invoice_email_pdf')
1244 and scalar($conf->config('invoice_email_pdf_note')) ) {
1246 warn "$me using 'invoice_email_pdf_note'"
1248 $return{'body'} = [ map { $_ . "\n" }
1249 $conf->config('invoice_email_pdf_note')
1254 warn "$me not using 'invoice_email_pdf_note'"
1256 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1257 $return{'body'} = $args{'print_text'};
1259 $return{'body'} = [ $self->print_text(\%opt) ];
1272 Returns a list suitable for passing to MIME::Entity->build(), representing
1273 this invoice as PDF attachment.
1280 'Type' => 'application/pdf',
1281 'Encoding' => 'base64',
1282 'Data' => [ $self->print_pdf(@_) ],
1283 'Disposition' => 'attachment',
1284 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1288 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1290 Sends this invoice to the destinations configured for this customer: sends
1291 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1293 Options can be passed as a hashref (recommended) or as a list of up to
1294 four values for templatename, agentnum, invoice_from and amount.
1296 I<template>, if specified, is the name of a suffix for alternate invoices.
1298 I<agentnum>, if specified, means that this invoice will only be sent for customers
1299 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1300 single agent) or an arrayref of agentnums.
1302 I<invoice_from>, if specified, overrides the default email invoice From: address.
1304 I<amount>, if specified, only sends the invoice if the total amount owed on this
1305 invoice and all older invoices is greater than the specified amount.
1307 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1311 sub queueable_send {
1314 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1315 or die "invalid invoice number: " . $opt{invnum};
1317 my @args = ( $opt{template}, $opt{agentnum} );
1318 push @args, $opt{invoice_from}
1319 if exists($opt{invoice_from}) && $opt{invoice_from};
1321 my $error = $self->send( @args );
1322 die $error if $error;
1328 my $conf = $self->conf;
1330 my( $template, $invoice_from, $notice_name );
1332 my $balance_over = 0;
1336 $template = $opt->{'template'} || '';
1337 if ( $agentnums = $opt->{'agentnum'} ) {
1338 $agentnums = [ $agentnums ] unless ref($agentnums);
1340 $invoice_from = $opt->{'invoice_from'};
1341 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1342 $notice_name = $opt->{'notice_name'};
1344 $template = scalar(@_) ? shift : '';
1345 if ( scalar(@_) && $_[0] ) {
1346 $agentnums = ref($_[0]) ? shift : [ shift ];
1348 $invoice_from = shift if scalar(@_);
1349 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1352 my $cust_main = $self->cust_main;
1354 return 'N/A' unless ! $agentnums
1355 or grep { $_ == $cust_main->agentnum } @$agentnums;
1358 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1360 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1361 $conf->config('invoice_from', $cust_main->agentnum );
1364 'template' => $template,
1365 'invoice_from' => $invoice_from,
1366 'notice_name' => ( $notice_name || 'Invoice' ),
1369 my @invoicing_list = $cust_main->invoicing_list;
1371 #$self->email_invoice(\%opt)
1373 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1374 && ! $self->invoice_noemail;
1376 #$self->print_invoice(\%opt)
1378 if grep { $_ eq 'POST' } @invoicing_list; #postal
1380 $self->fax_invoice(\%opt)
1381 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1387 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1389 Emails this invoice.
1391 Options can be passed as a hashref (recommended) or as a list of up to
1392 two values for templatename and invoice_from.
1394 I<template>, if specified, is the name of a suffix for alternate invoices.
1396 I<invoice_from>, if specified, overrides the default email invoice From: address.
1398 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1402 sub queueable_email {
1405 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1406 or die "invalid invoice number: " . $opt{invnum};
1408 my %args = ( 'template' => $opt{template} );
1409 $args{$_} = $opt{$_}
1410 foreach grep { exists($opt{$_}) && $opt{$_} }
1411 qw( invoice_from notice_name no_coupon );
1413 my $error = $self->email( \%args );
1414 die $error if $error;
1418 #sub email_invoice {
1421 return if $self->hide;
1422 my $conf = $self->conf;
1424 my( $template, $invoice_from, $notice_name, $no_coupon );
1427 $template = $opt->{'template'} || '';
1428 $invoice_from = $opt->{'invoice_from'};
1429 $notice_name = $opt->{'notice_name'} || 'Invoice';
1430 $no_coupon = $opt->{'no_coupon'} || 0;
1432 $template = scalar(@_) ? shift : '';
1433 $invoice_from = shift if scalar(@_);
1434 $notice_name = 'Invoice';
1438 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1439 $conf->config('invoice_from', $self->cust_main->agentnum );
1441 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1442 $self->cust_main->invoicing_list;
1444 if ( ! @invoicing_list ) { #no recipients
1445 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1446 die 'No recipients for customer #'. $self->custnum;
1448 #default: better to notify this person than silence
1449 @invoicing_list = ($invoice_from);
1453 my $subject = $self->email_subject($template);
1455 my $error = send_email(
1456 $self->generate_email(
1457 'from' => $invoice_from,
1458 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1459 'subject' => $subject,
1460 'template' => $template,
1461 'notice_name' => $notice_name,
1462 'no_coupon' => $no_coupon,
1465 die "can't email invoice: $error\n" if $error;
1466 #die "$error\n" if $error;
1472 my $conf = $self->conf;
1474 #my $template = scalar(@_) ? shift : '';
1477 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1480 my $cust_main = $self->cust_main;
1481 my $name = $cust_main->name;
1482 my $name_short = $cust_main->name_short;
1483 my $invoice_number = $self->invnum;
1484 my $invoice_date = $self->_date_pretty;
1486 eval qq("$subject");
1489 =item lpr_data HASHREF | [ TEMPLATE ]
1491 Returns the postscript or plaintext for this invoice as an arrayref.
1493 Options can be passed as a hashref (recommended) or as a single optional value
1496 I<template>, if specified, is the name of a suffix for alternate invoices.
1498 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1504 my $conf = $self->conf;
1505 my( $template, $notice_name );
1508 $template = $opt->{'template'} || '';
1509 $notice_name = $opt->{'notice_name'} || 'Invoice';
1511 $template = scalar(@_) ? shift : '';
1512 $notice_name = 'Invoice';
1516 'template' => $template,
1517 'notice_name' => $notice_name,
1520 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1521 [ $self->$method( \%opt ) ];
1524 =item print HASHREF | [ TEMPLATE ]
1526 Prints this invoice.
1528 Options can be passed as a hashref (recommended) or as a single optional
1531 I<template>, if specified, is the name of a suffix for alternate invoices.
1533 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1537 #sub print_invoice {
1540 return if $self->hide;
1541 my $conf = $self->conf;
1543 my( $template, $notice_name );
1546 $template = $opt->{'template'} || '';
1547 $notice_name = $opt->{'notice_name'} || 'Invoice';
1549 $template = scalar(@_) ? shift : '';
1550 $notice_name = 'Invoice';
1554 'template' => $template,
1555 'notice_name' => $notice_name,
1558 if($conf->exists('invoice_print_pdf')) {
1559 # Add the invoice to the current batch.
1560 $self->batch_invoice(\%opt);
1564 $self->lpr_data(\%opt),
1565 'agentnum' => $self->cust_main->agentnum,
1570 =item fax_invoice HASHREF | [ TEMPLATE ]
1574 Options can be passed as a hashref (recommended) or as a single optional
1577 I<template>, if specified, is the name of a suffix for alternate invoices.
1579 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1585 return if $self->hide;
1586 my $conf = $self->conf;
1588 my( $template, $notice_name );
1591 $template = $opt->{'template'} || '';
1592 $notice_name = $opt->{'notice_name'} || 'Invoice';
1594 $template = scalar(@_) ? shift : '';
1595 $notice_name = 'Invoice';
1598 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1599 unless $conf->exists('invoice_latex');
1601 my $dialstring = $self->cust_main->getfield('fax');
1605 'template' => $template,
1606 'notice_name' => $notice_name,
1609 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1610 'dialstring' => $dialstring,
1612 die $error if $error;
1616 =item batch_invoice [ HASHREF ]
1618 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1619 isn't an open batch, one will be created.
1624 my ($self, $opt) = @_;
1625 my $bill_batch = $self->get_open_bill_batch;
1626 my $cust_bill_batch = FS::cust_bill_batch->new({
1627 batchnum => $bill_batch->batchnum,
1628 invnum => $self->invnum,
1630 return $cust_bill_batch->insert($opt);
1633 =item get_open_batch
1635 Returns the currently open batch as an FS::bill_batch object, creating a new
1636 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1641 sub get_open_bill_batch {
1643 my $conf = $self->conf;
1644 my $hashref = { status => 'O' };
1645 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1646 ? $self->cust_main->agentnum
1648 my $batch = qsearchs('bill_batch', $hashref);
1649 return $batch if $batch;
1650 $batch = FS::bill_batch->new($hashref);
1651 my $error = $batch->insert;
1652 die $error if $error;
1656 =item ftp_invoice [ TEMPLATENAME ]
1658 Sends this invoice data via FTP.
1660 TEMPLATENAME is unused?
1666 my $conf = $self->conf;
1667 my $template = scalar(@_) ? shift : '';
1670 'protocol' => 'ftp',
1671 'server' => $conf->config('cust_bill-ftpserver'),
1672 'username' => $conf->config('cust_bill-ftpusername'),
1673 'password' => $conf->config('cust_bill-ftppassword'),
1674 'dir' => $conf->config('cust_bill-ftpdir'),
1675 'format' => $conf->config('cust_bill-ftpformat'),
1679 =item spool_invoice [ TEMPLATENAME ]
1681 Spools this invoice data (see L<FS::spool_csv>)
1683 TEMPLATENAME is unused?
1689 my $conf = $self->conf;
1690 my $template = scalar(@_) ? shift : '';
1693 'format' => $conf->config('cust_bill-spoolformat'),
1694 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1698 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1700 Like B<send>, but only sends the invoice if it is the newest open invoice for
1705 sub send_if_newest {
1710 grep { $_->owed > 0 }
1711 qsearch('cust_bill', {
1712 'custnum' => $self->custnum,
1713 #'_date' => { op=>'>', value=>$self->_date },
1714 'invnum' => { op=>'>', value=>$self->invnum },
1721 =item send_csv OPTION => VALUE, ...
1723 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1727 protocol - currently only "ftp"
1733 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1734 and YYMMDDHHMMSS is a timestamp.
1736 See L</print_csv> for a description of the output format.
1741 my($self, %opt) = @_;
1745 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1746 mkdir $spooldir, 0700 unless -d $spooldir;
1748 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1749 my $file = "$spooldir/$tracctnum.csv";
1751 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1753 open(CSV, ">$file") or die "can't open $file: $!";
1761 if ( $opt{protocol} eq 'ftp' ) {
1762 eval "use Net::FTP;";
1764 $net = Net::FTP->new($opt{server}) or die @$;
1766 die "unknown protocol: $opt{protocol}";
1769 $net->login( $opt{username}, $opt{password} )
1770 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1772 $net->binary or die "can't set binary mode";
1774 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1776 $net->put($file) or die "can't put $file: $!";
1786 Spools CSV invoice data.
1792 =item format - 'default' or 'billco'
1794 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1796 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1798 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1805 my($self, %opt) = @_;
1807 my $cust_main = $self->cust_main;
1809 if ( $opt{'dest'} ) {
1810 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1811 $cust_main->invoicing_list;
1812 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1813 || ! keys %invoicing_list;
1816 if ( $opt{'balanceover'} ) {
1818 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1821 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1822 mkdir $spooldir, 0700 unless -d $spooldir;
1824 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1828 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1829 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1832 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1834 open(CSV, ">>$file") or die "can't open $file: $!";
1835 flock(CSV, LOCK_EX);
1840 if ( lc($opt{'format'}) eq 'billco' ) {
1842 flock(CSV, LOCK_UN);
1847 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1850 open(CSV,">>$file") or die "can't open $file: $!";
1851 flock(CSV, LOCK_EX);
1857 flock(CSV, LOCK_UN);
1864 =item print_csv OPTION => VALUE, ...
1866 Returns CSV data for this invoice.
1870 format - 'default' or 'billco'
1872 Returns a list consisting of two scalars. The first is a single line of CSV
1873 header information for this invoice. The second is one or more lines of CSV
1874 detail information for this invoice.
1876 If I<format> is not specified or "default", the fields of the CSV file are as
1879 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1883 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1885 B<record_type> is C<cust_bill> for the initial header line only. The
1886 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1887 fields are filled in.
1889 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1890 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1893 =item invnum - invoice number
1895 =item custnum - customer number
1897 =item _date - invoice date
1899 =item charged - total invoice amount
1901 =item first - customer first name
1903 =item last - customer first name
1905 =item company - company name
1907 =item address1 - address line 1
1909 =item address2 - address line 1
1919 =item pkg - line item description
1921 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1923 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1925 =item sdate - start date for recurring fee
1927 =item edate - end date for recurring fee
1931 If I<format> is "billco", the fields of the header CSV file are as follows:
1933 +-------------------------------------------------------------------+
1934 | FORMAT HEADER FILE |
1935 |-------------------------------------------------------------------|
1936 | Field | Description | Name | Type | Width |
1937 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1938 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1939 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1940 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1941 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1942 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1943 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1944 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1945 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1946 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1947 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1948 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1949 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1950 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1951 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1952 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1953 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1954 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1955 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1956 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1957 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1958 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1959 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1960 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1961 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1962 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1963 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1964 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1965 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1966 +-------+-------------------------------+------------+------+-------+
1968 If I<format> is "billco", the fields of the detail CSV file are as follows:
1970 FORMAT FOR DETAIL FILE
1972 Field | Description | Name | Type | Width
1973 1 | N/A-Leave Empty | RC | CHAR | 2
1974 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1975 3 | Account Number | TRACCTNUM | CHAR | 15
1976 4 | Invoice Number | TRINVOICE | CHAR | 15
1977 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1978 6 | Transaction Detail | DETAILS | CHAR | 100
1979 7 | Amount | AMT | NUM* | 9
1980 8 | Line Format Control** | LNCTRL | CHAR | 2
1981 9 | Grouping Code | GROUP | CHAR | 2
1982 10 | User Defined | ACCT CODE | CHAR | 15
1987 my($self, %opt) = @_;
1989 eval "use Text::CSV_XS";
1992 my $cust_main = $self->cust_main;
1994 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1996 if ( lc($opt{'format'}) eq 'billco' ) {
1999 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
2001 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
2003 my( $previous_balance, @unused ) = $self->previous; #previous balance
2005 my $pmt_cr_applied = 0;
2006 $pmt_cr_applied += $_->{'amount'}
2007 foreach ( $self->_items_payments, $self->_items_credits ) ;
2009 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2012 '', # 1 | N/A-Leave Empty CHAR 2
2013 '', # 2 | N/A-Leave Empty CHAR 15
2014 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
2015 $self->invnum, # 4 | Transaction Invoice No CHAR 15
2016 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
2017 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
2018 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2019 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2020 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2021 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2022 '', # 10 | Ancillary Billing Information CHAR 30
2023 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2024 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2027 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2030 $duedate, # 14 | Bill Due Date CHAR 10
2032 $previous_balance, # 15 | Previous Balance NUM* 9
2033 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2034 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2035 $totaldue, # 18 | Total Amt Due NUM* 9
2036 $totaldue, # 19 | Total Amt Due NUM* 9
2037 '', # 20 | 30 Day Aging NUM* 9
2038 '', # 21 | 60 Day Aging NUM* 9
2039 '', # 22 | 90 Day Aging NUM* 9
2040 'N', # 23 | Y/N CHAR 1
2041 '', # 24 | Remittance automation CHAR 100
2042 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2043 $self->custnum, # 26 | Customer Reference Number CHAR 15
2044 '0', # 27 | Federal Tax*** NUM* 9
2045 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2046 '0', # 29 | Other Taxes & Fees*** NUM* 9
2049 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2051 my ($previous_balance) = $self->previous;
2052 $previous_balance = sprintf('%.2f', $previous_balance);
2053 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2059 $self->_items_pkg, #_items_nontax? no sections or anything
2064 $cust_main->agentnum,
2065 $cust_main->agent->agent,
2069 $cust_main->company,
2070 $cust_main->address1,
2071 $cust_main->address2,
2077 time2str("%x", $self->_date),
2082 $self->due_date2str("%x"),
2093 time2str("%x", $self->_date),
2094 sprintf("%.2f", $self->charged),
2095 ( map { $cust_main->getfield($_) }
2096 qw( first last company address1 address2 city state zip country ) ),
2098 ) or die "can't create csv";
2101 my $header = $csv->string. "\n";
2104 if ( lc($opt{'format'}) eq 'billco' ) {
2107 foreach my $item ( $self->_items_pkg ) {
2110 '', # 1 | N/A-Leave Empty CHAR 2
2111 '', # 2 | N/A-Leave Empty CHAR 15
2112 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2113 $self->invnum, # 4 | Invoice Number CHAR 15
2114 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2115 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2116 $item->{'amount'}, # 7 | Amount NUM* 9
2117 '', # 8 | Line Format Control** CHAR 2
2118 '', # 9 | Grouping Code CHAR 2
2119 '', # 10 | User Defined CHAR 15
2122 $detail .= $csv->string. "\n";
2126 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2132 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2134 my($pkg, $setup, $recur, $sdate, $edate);
2135 if ( $cust_bill_pkg->pkgnum ) {
2137 ($pkg, $setup, $recur, $sdate, $edate) = (
2138 $cust_bill_pkg->part_pkg->pkg,
2139 ( $cust_bill_pkg->setup != 0
2140 ? sprintf("%.2f", $cust_bill_pkg->setup )
2142 ( $cust_bill_pkg->recur != 0
2143 ? sprintf("%.2f", $cust_bill_pkg->recur )
2145 ( $cust_bill_pkg->sdate
2146 ? time2str("%x", $cust_bill_pkg->sdate)
2148 ($cust_bill_pkg->edate
2149 ?time2str("%x", $cust_bill_pkg->edate)
2153 } else { #pkgnum tax
2154 next unless $cust_bill_pkg->setup != 0;
2155 $pkg = $cust_bill_pkg->desc;
2156 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2157 ( $sdate, $edate ) = ( '', '' );
2163 ( map { '' } (1..11) ),
2164 ($pkg, $setup, $recur, $sdate, $edate)
2165 ) or die "can't create csv";
2167 $detail .= $csv->string. "\n";
2173 ( $header, $detail );
2179 Pays this invoice with a compliemntary payment. If there is an error,
2180 returns the error, otherwise returns false.
2186 my $cust_pay = new FS::cust_pay ( {
2187 'invnum' => $self->invnum,
2188 'paid' => $self->owed,
2191 'payinfo' => $self->cust_main->payinfo,
2199 Attempts to pay this invoice with a credit card payment via a
2200 Business::OnlinePayment realtime gateway. See
2201 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2202 for supported processors.
2208 $self->realtime_bop( 'CC', @_ );
2213 Attempts to pay this invoice with an electronic check (ACH) payment via a
2214 Business::OnlinePayment realtime gateway. See
2215 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2216 for supported processors.
2222 $self->realtime_bop( 'ECHECK', @_ );
2227 Attempts to pay this invoice with phone bill (LEC) payment via a
2228 Business::OnlinePayment realtime gateway. See
2229 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2230 for supported processors.
2236 $self->realtime_bop( 'LEC', @_ );
2240 my( $self, $method ) = (shift,shift);
2241 my $conf = $self->conf;
2244 my $cust_main = $self->cust_main;
2245 my $balance = $cust_main->balance;
2246 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2247 $amount = sprintf("%.2f", $amount);
2248 return "not run (balance $balance)" unless $amount > 0;
2250 my $description = 'Internet Services';
2251 if ( $conf->exists('business-onlinepayment-description') ) {
2252 my $dtempl = $conf->config('business-onlinepayment-description');
2254 my $agent_obj = $cust_main->agent
2255 or die "can't retreive agent for $cust_main (agentnum ".
2256 $cust_main->agentnum. ")";
2257 my $agent = $agent_obj->agent;
2258 my $pkgs = join(', ',
2259 map { $_->part_pkg->pkg }
2260 grep { $_->pkgnum } $self->cust_bill_pkg
2262 $description = eval qq("$dtempl");
2265 $cust_main->realtime_bop($method, $amount,
2266 'description' => $description,
2267 'invnum' => $self->invnum,
2268 #this didn't do what we want, it just calls apply_payments_and_credits
2270 'apply_to_invoice' => 1,
2273 #this changes application behavior: auto payments
2274 #triggered against a specific invoice are now applied
2275 #to that invoice instead of oldest open.
2281 =item batch_card OPTION => VALUE...
2283 Adds a payment for this invoice to the pending credit card batch (see
2284 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2285 runs the payment using a realtime gateway.
2290 my ($self, %options) = @_;
2291 my $cust_main = $self->cust_main;
2293 $options{invnum} = $self->invnum;
2295 $cust_main->batch_card(%options);
2298 sub _agent_template {
2300 $self->cust_main->agent_template;
2303 sub _agent_invoice_from {
2305 $self->cust_main->agent_invoice_from;
2308 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2310 Returns an text invoice, as a list of lines.
2312 Options can be passed as a hashref (recommended) or as a list of time, template
2313 and then any key/value pairs for any other options.
2315 I<time>, if specified, is used to control the printing of overdue messages. The
2316 default is now. It isn't the date of the invoice; that's the `_date' field.
2317 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2318 L<Time::Local> and L<Date::Parse> for conversion functions.
2320 I<template>, if specified, is the name of a suffix for alternate invoices.
2322 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2328 my( $today, $template, %opt );
2330 %opt = %{ shift() };
2331 $today = delete($opt{'time'}) || '';
2332 $template = delete($opt{template}) || '';
2334 ( $today, $template, %opt ) = @_;
2337 my %params = ( 'format' => 'template' );
2338 $params{'time'} = $today if $today;
2339 $params{'template'} = $template if $template;
2340 $params{$_} = $opt{$_}
2341 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2343 $self->print_generic( %params );
2346 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2348 Internal method - returns a filename of a filled-in LaTeX template for this
2349 invoice (Note: add ".tex" to get the actual filename), and a filename of
2350 an associated logo (with the .eps extension included).
2352 See print_ps and print_pdf for methods that return PostScript and PDF output.
2354 Options can be passed as a hashref (recommended) or as a list of time, template
2355 and then any key/value pairs for any other options.
2357 I<time>, if specified, is used to control the printing of overdue messages. The
2358 default is now. It isn't the date of the invoice; that's the `_date' field.
2359 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2360 L<Time::Local> and L<Date::Parse> for conversion functions.
2362 I<template>, if specified, is the name of a suffix for alternate invoices.
2364 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2370 my $conf = $self->conf;
2371 my( $today, $template, %opt );
2373 %opt = %{ shift() };
2374 $today = delete($opt{'time'}) || '';
2375 $template = delete($opt{template}) || '';
2377 ( $today, $template, %opt ) = @_;
2380 my %params = ( 'format' => 'latex' );
2381 $params{'time'} = $today if $today;
2382 $params{'template'} = $template if $template;
2383 $params{$_} = $opt{$_}
2384 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2386 $template ||= $self->_agent_template;
2388 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2389 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2393 ) or die "can't open temp file: $!\n";
2395 my $agentnum = $self->cust_main->agentnum;
2397 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2398 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2399 or die "can't write temp file: $!\n";
2401 print $lh $conf->config_binary('logo.eps', $agentnum)
2402 or die "can't write temp file: $!\n";
2405 $params{'logo_file'} = $lh->filename;
2407 if($conf->exists('invoice-barcode')){
2408 my $png_file = $self->invoice_barcode($dir);
2409 my $eps_file = $png_file;
2410 $eps_file =~ s/\.png$/.eps/g;
2411 $png_file =~ /(barcode.*png)/;
2413 $eps_file =~ /(barcode.*eps)/;
2416 my $curr_dir = cwd();
2418 # after painfuly long experimentation, it was determined that sam2p won't
2419 # accept : and other chars in the path, no matter how hard I tried to
2420 # escape them, hence the chdir (and chdir back, just to be safe)
2421 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2422 or die "sam2p failed: $!\n";
2426 $params{'barcode_file'} = $eps_file;
2429 my @filled_in = $self->print_generic( %params );
2431 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2435 ) or die "can't open temp file: $!\n";
2436 binmode($fh, ':utf8'); # language support
2437 print $fh join('', @filled_in );
2440 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2441 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2445 =item invoice_barcode DIR_OR_FALSE
2447 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2448 it is taken as the temp directory where the PNG file will be generated and the
2449 PNG file name is returned. Otherwise, the PNG image itself is returned.
2453 sub invoice_barcode {
2454 my ($self, $dir) = (shift,shift);
2456 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2457 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2458 my $gd = $gdbar->plot(Height => 30);
2461 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2465 ) or die "can't open temp file: $!\n";
2466 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2467 my $png_file = $bh->filename;
2474 =item print_generic OPTION => VALUE ...
2476 Internal method - returns a filled-in template for this invoice as a scalar.
2478 See print_ps and print_pdf for methods that return PostScript and PDF output.
2480 Non optional options include
2481 format - latex, html, template
2483 Optional options include
2485 template - a value used as a suffix for a configuration template
2487 time - a value used to control the printing of overdue messages. The
2488 default is now. It isn't the date of the invoice; that's the `_date' field.
2489 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2490 L<Time::Local> and L<Date::Parse> for conversion functions.
2494 unsquelch_cdr - overrides any per customer cdr squelching when true
2496 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2498 locale - override customer's locale
2502 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2503 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2504 # yes: fixed width/plain text printing will be borked
2506 my( $self, %params ) = @_;
2507 my $conf = $self->conf;
2508 my $today = $params{today} ? $params{today} : time;
2509 warn "$me print_generic called on $self with suffix $params{template}\n"
2512 my $format = $params{format};
2513 die "Unknown format: $format"
2514 unless $format =~ /^(latex|html|template)$/;
2516 my $cust_main = $self->cust_main;
2517 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2518 unless $cust_main->payname
2519 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2521 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2522 'html' => [ '<%=', '%>' ],
2523 'template' => [ '{', '}' ],
2526 warn "$me print_generic creating template\n"
2529 #create the template
2530 my $template = $params{template} ? $params{template} : $self->_agent_template;
2531 my $templatefile = "invoice_$format";
2532 $templatefile .= "_$template"
2533 if length($template) && $conf->exists($templatefile."_$template");
2534 my @invoice_template = map "$_\n", $conf->config($templatefile)
2535 or die "cannot load config data $templatefile";
2538 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2539 #change this to a die when the old code is removed
2540 warn "old-style invoice template $templatefile; ".
2541 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2542 $old_latex = 'true';
2543 @invoice_template = _translate_old_latex_format(@invoice_template);
2546 warn "$me print_generic creating T:T object\n"
2549 my $text_template = new Text::Template(
2551 SOURCE => \@invoice_template,
2552 DELIMITERS => $delimiters{$format},
2555 warn "$me print_generic compiling T:T object\n"
2558 $text_template->compile()
2559 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2562 # additional substitution could possibly cause breakage in existing templates
2563 my %convert_maps = (
2565 'notes' => sub { map "$_", @_ },
2566 'footer' => sub { map "$_", @_ },
2567 'smallfooter' => sub { map "$_", @_ },
2568 'returnaddress' => sub { map "$_", @_ },
2569 'coupon' => sub { map "$_", @_ },
2570 'summary' => sub { map "$_", @_ },
2576 s/%%(.*)$/<!-- $1 -->/g;
2577 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2578 s/\\begin\{enumerate\}/<ol>/g;
2580 s/\\end\{enumerate\}/<\/ol>/g;
2581 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2590 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2592 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2597 s/\\\\\*?\s*$/<BR>/;
2598 s/\\hyphenation\{[\w\s\-]+}//;
2603 'coupon' => sub { "" },
2604 'summary' => sub { "" },
2611 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2612 s/\\begin\{enumerate\}//g;
2614 s/\\end\{enumerate\}//g;
2615 s/\\textbf\{(.*)\}/$1/g;
2622 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2624 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2629 s/\\\\\*?\s*$/\n/; # dubious
2630 s/\\hyphenation\{[\w\s\-]+}//;
2634 'coupon' => sub { "" },
2635 'summary' => sub { "" },
2640 # hashes for differing output formats
2641 my %nbsps = ( 'latex' => '~',
2642 'html' => '', # '&nbps;' would be nice
2643 'template' => '', # not used
2645 my $nbsp = $nbsps{$format};
2647 my %escape_functions = ( 'latex' => \&_latex_escape,
2648 'html' => \&_html_escape_nbsp,#\&encode_entities,
2649 'template' => sub { shift },
2651 my $escape_function = $escape_functions{$format};
2652 my $escape_function_nonbsp = ($format eq 'html')
2653 ? \&_html_escape : $escape_function;
2655 my %date_formats = ( 'latex' => $date_format_long,
2656 'html' => $date_format_long,
2659 $date_formats{'html'} =~ s/ / /g;
2661 my $date_format = $date_formats{$format};
2663 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2665 'html' => sub { return '<b>'. shift(). '</b>'
2667 'template' => sub { shift },
2669 my $embolden_function = $embolden_functions{$format};
2671 my %newline_tokens = ( 'latex' => '\\\\',
2675 my $newline_token = $newline_tokens{$format};
2677 warn "$me generating template variables\n"
2680 # generate template variables
2683 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2687 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2693 $returnaddress = join("\n",
2694 $conf->config_orbase("invoice_${format}returnaddress", $template)
2697 } elsif ( grep /\S/,
2698 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2700 my $convert_map = $convert_maps{$format}{'returnaddress'};
2703 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2708 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2710 my $convert_map = $convert_maps{$format}{'returnaddress'};
2711 $returnaddress = join( "\n", &$convert_map(
2712 map { s/( {2,})/'~' x length($1)/eg;
2716 ( $conf->config('company_name', $self->cust_main->agentnum),
2717 $conf->config('company_address', $self->cust_main->agentnum),
2724 my $warning = "Couldn't find a return address; ".
2725 "do you need to set the company_address configuration value?";
2727 $returnaddress = $nbsp;
2728 #$returnaddress = $warning;
2732 warn "$me generating invoice data\n"
2735 my $agentnum = $self->cust_main->agentnum;
2737 my %invoice_data = (
2740 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2741 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2742 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2743 'returnaddress' => $returnaddress,
2744 'agent' => &$escape_function($cust_main->agent->agent),
2747 'invnum' => $self->invnum,
2748 '_date' => $self->_date,
2749 'date' => time2str($date_format, $self->_date),
2750 'today' => time2str($date_format_long, $today),
2751 'terms' => $self->terms,
2752 'template' => $template, #params{'template'},
2753 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2754 'current_charges' => sprintf("%.2f", $self->charged),
2755 'duedate' => $self->due_date2str($rdate_format), #date_format?
2758 'custnum' => $cust_main->display_custnum,
2759 'agent_custid' => &$escape_function($cust_main->agent_custid),
2760 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2761 payname company address1 address2 city state zip fax
2765 'ship_enable' => $conf->exists('invoice-ship_address'),
2766 'unitprices' => $conf->exists('invoice-unitprice'),
2767 'smallernotes' => $conf->exists('invoice-smallernotes'),
2768 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2769 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2771 #layout info -- would be fancy to calc some of this and bury the template
2773 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2774 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2775 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2776 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2777 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2778 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2779 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2780 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2781 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2782 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2784 # better hang on to conf_dir for a while (for old templates)
2785 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2787 #these are only used when doing paged plaintext
2794 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2795 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2796 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2797 # eval to avoid death for unimplemented languages
2798 my $dh = eval { Date::Language->new($info{'name'}) } ||
2799 Date::Language->new(); # fall back to English
2800 # prototype here to silence warnings
2801 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2802 # eventually use this date handle everywhere in here, too
2804 my $min_sdate = 999999999999;
2806 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2807 next unless $cust_bill_pkg->pkgnum > 0;
2808 $min_sdate = $cust_bill_pkg->sdate
2809 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2810 $max_edate = $cust_bill_pkg->edate
2811 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2814 $invoice_data{'bill_period'} = '';
2815 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2816 . " to " . time2str('%e %h', $max_edate)
2817 if ($max_edate != 0 && $min_sdate != 999999999999);
2819 $invoice_data{finance_section} = '';
2820 if ( $conf->config('finance_pkgclass') ) {
2822 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2823 $invoice_data{finance_section} = $pkg_class->categoryname;
2825 $invoice_data{finance_amount} = '0.00';
2826 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2828 my $countrydefault = $conf->config('countrydefault') || 'US';
2829 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2830 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2831 my $method = $prefix.$_;
2832 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2834 $invoice_data{'ship_country'} = ''
2835 if ( $invoice_data{'ship_country'} eq $countrydefault );
2837 $invoice_data{'cid'} = $params{'cid'}
2840 if ( $cust_main->country eq $countrydefault ) {
2841 $invoice_data{'country'} = '';
2843 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2847 $invoice_data{'address'} = \@address;
2849 $cust_main->payname.
2850 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2851 ? " (P.O. #". $cust_main->payinfo. ")"
2855 push @address, $cust_main->company
2856 if $cust_main->company;
2857 push @address, $cust_main->address1;
2858 push @address, $cust_main->address2
2859 if $cust_main->address2;
2861 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2862 push @address, $invoice_data{'country'}
2863 if $invoice_data{'country'};
2865 while (scalar(@address) < 5);
2867 $invoice_data{'logo_file'} = $params{'logo_file'}
2868 if $params{'logo_file'};
2869 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2870 if $params{'barcode_file'};
2871 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2872 if $params{'barcode_img'};
2873 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2874 if $params{'barcode_cid'};
2876 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2877 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2878 #my $balance_due = $self->owed + $pr_total - $cr_total;
2879 my $balance_due = $self->owed + $pr_total;
2881 # the customer's current balance as shown on the invoice before this one
2882 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2884 # the change in balance from that invoice to this one
2885 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2887 # the sum of amount owed on all previous invoices
2888 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2890 # the sum of amount owed on all invoices
2891 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2893 # info from customer's last invoice before this one, for some
2895 $invoice_data{'last_bill'} = {};
2896 if ( $self->previous_bill ) {
2897 $invoice_data{'last_bill'} = {
2898 '_date' => $self->previous_bill->_date, #unformatted
2899 # all we need for now
2903 my $summarypage = '';
2904 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2907 $invoice_data{'summarypage'} = $summarypage;
2909 warn "$me substituting variables in notes, footer, smallfooter\n"
2912 my @include = (qw( notes footer smallfooter ));
2913 push @include, 'coupon' unless $params{'no_coupon'};
2914 foreach my $include (@include) {
2916 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2919 if ( $conf->exists($inc_file, $agentnum)
2920 && length( $conf->config($inc_file, $agentnum) ) ) {
2922 @inc_src = $conf->config($inc_file, $agentnum);
2926 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2928 my $convert_map = $convert_maps{$format}{$include};
2930 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2931 s/--\@\]/$delimiters{$format}[1]/g;
2934 &$convert_map( $conf->config($inc_file, $agentnum) );
2938 my $inc_tt = new Text::Template (
2940 SOURCE => [ map "$_\n", @inc_src ],
2941 DELIMITERS => $delimiters{$format},
2942 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2944 unless ( $inc_tt->compile() ) {
2945 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2946 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2950 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2952 $invoice_data{$include} =~ s/\n+$//
2953 if ($format eq 'latex');
2956 # let invoices use either of these as needed
2957 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2958 ? $cust_main->payinfo : '';
2959 $invoice_data{'po_line'} =
2960 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2961 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2964 my %money_chars = ( 'latex' => '',
2965 'html' => $conf->config('money_char') || '$',
2968 my $money_char = $money_chars{$format};
2970 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2971 'html' => $conf->config('money_char') || '$',
2974 my $other_money_char = $other_money_chars{$format};
2975 $invoice_data{'dollar'} = $other_money_char;
2977 my @detail_items = ();
2978 my @total_items = ();
2982 $invoice_data{'detail_items'} = \@detail_items;
2983 $invoice_data{'total_items'} = \@total_items;
2984 $invoice_data{'buf'} = \@buf;
2985 $invoice_data{'sections'} = \@sections;
2987 warn "$me generating sections\n"
2990 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2991 'subtotal' => $other_money_char.
2992 sprintf('%.2f', $pr_total),
2993 'summarized' => '', #why? $summarypage ? 'Y' : '',
2995 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2996 join(' / ', map { $cust_main->balance_date_range(@$_) }
2997 $self->_prior_month30s
2999 if $conf->exists('invoice_include_aging');
3002 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
3003 'subtotal' => $taxtotal, # adjusted below
3006 my $tax_weight = _pkg_category($tax_section->{description})
3007 ? _pkg_category($tax_section->{description})->weight
3009 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
3010 $tax_section->{'sort_weight'} = $tax_weight;
3013 my $adjusttotal = 0;
3014 my $adjust_section = {
3015 'description' => $self->mt('Credits, Payments, and Adjustments'),
3016 'adjust_section' => 1,
3017 'subtotal' => 0, # adjusted below
3019 my $adjust_weight = _pkg_category($adjust_section->{description})
3020 ? _pkg_category($adjust_section->{description})->weight
3022 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3023 $adjust_section->{'sort_weight'} = $adjust_weight;
3025 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3026 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3027 $invoice_data{'multisection'} = $multisection;
3028 my $late_sections = [];
3029 my $extra_sections = [];
3030 my $extra_lines = ();
3032 my $default_section = { 'description' => '',
3037 if ( $multisection ) {
3038 ($extra_sections, $extra_lines) =
3039 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3040 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3042 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3044 push @detail_items, @$extra_lines if $extra_lines;
3046 $self->_items_sections( $late_sections, # this could stand a refactor
3048 $escape_function_nonbsp,
3052 if ($conf->exists('svc_phone_sections')) {
3053 my ($phone_sections, $phone_lines) =
3054 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3055 push @{$late_sections}, @$phone_sections;
3056 push @detail_items, @$phone_lines;
3058 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3059 my ($accountcode_section, $accountcode_lines) =
3060 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3061 if ( scalar(@$accountcode_lines) ) {
3062 push @{$late_sections}, $accountcode_section;
3063 push @detail_items, @$accountcode_lines;
3066 } else {# not multisection
3067 # make a default section
3068 push @sections, $default_section;
3069 # and calculate the finance charge total, since it won't get done otherwise.
3070 # XXX possibly other totals?
3071 # XXX possibly finance_pkgclass should not be used in this manner?
3072 if ( $conf->exists('finance_pkgclass') ) {
3073 my @finance_charges;
3074 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3075 if ( grep { $_->section eq $invoice_data{finance_section} }
3076 $cust_bill_pkg->cust_bill_pkg_display ) {
3077 # I think these are always setup fees, but just to be sure...
3078 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3081 $invoice_data{finance_amount} =
3082 sprintf('%.2f', sum( @finance_charges ) || 0);
3086 # previous invoice balances in the Previous Charges section if there
3087 # is one, otherwise in the main detail section
3088 if ( $self->can('_items_previous') &&
3089 $self->enable_previous &&
3090 ! $conf->exists('previous_balance-summary_only') ) {
3092 warn "$me adding previous balances\n"
3095 foreach my $line_item ( $self->_items_previous ) {
3098 ext_description => [],
3100 $detail->{'ref'} = $line_item->{'pkgnum'};
3101 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3102 $detail->{'quantity'} = 1;
3103 $detail->{'section'} = $multisection ? $previous_section
3105 $detail->{'description'} = &$escape_function($line_item->{'description'});
3106 if ( exists $line_item->{'ext_description'} ) {
3107 @{$detail->{'ext_description'}} = map {
3108 &$escape_function($_);
3109 } @{$line_item->{'ext_description'}};
3111 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3112 $line_item->{'amount'};
3113 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3115 push @detail_items, $detail;
3116 push @buf, [ $detail->{'description'},
3117 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3123 if ( @pr_cust_bill && $self->enable_previous ) {
3124 push @buf, ['','-----------'];
3125 push @buf, [ $self->mt('Total Previous Balance'),
3126 $money_char. sprintf("%10.2f", $pr_total) ];
3130 if ( $conf->exists('svc_phone-did-summary') ) {
3131 warn "$me adding DID summary\n"
3134 my ($didsummary,$minutes) = $self->_did_summary;
3135 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3137 { 'description' => $didsummary_desc,
3138 'ext_description' => [ $didsummary, $minutes ],
3142 foreach my $section (@sections, @$late_sections) {
3144 warn "$me adding section \n". Dumper($section)
3147 # begin some normalization
3148 $section->{'subtotal'} = $section->{'amount'}
3150 && !exists($section->{subtotal})
3151 && exists($section->{amount});
3153 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3154 if ( $invoice_data{finance_section} &&
3155 $section->{'description'} eq $invoice_data{finance_section} );
3157 $section->{'subtotal'} = $other_money_char.
3158 sprintf('%.2f', $section->{'subtotal'})
3161 # continue some normalization
3162 $section->{'amount'} = $section->{'subtotal'}
3166 if ( $section->{'description'} ) {
3167 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3172 warn "$me setting options\n"
3175 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3177 $options{'section'} = $section if $multisection;
3178 $options{'format'} = $format;
3179 $options{'escape_function'} = $escape_function;
3180 $options{'no_usage'} = 1 unless $unsquelched;
3181 $options{'unsquelched'} = $unsquelched;
3182 $options{'summary_page'} = $summarypage;
3183 $options{'skip_usage'} =
3184 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3185 $options{'multilocation'} = $multilocation;
3186 $options{'multisection'} = $multisection;
3188 warn "$me searching for line items\n"
3191 foreach my $line_item ( $self->_items_pkg(%options) ) {
3193 warn "$me adding line item $line_item\n"
3197 ext_description => [],
3199 $detail->{'ref'} = $line_item->{'pkgnum'};
3200 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3201 $detail->{'quantity'} = $line_item->{'quantity'};
3202 $detail->{'section'} = $section;
3203 $detail->{'description'} = &$escape_function($line_item->{'description'});
3204 if ( exists $line_item->{'ext_description'} ) {
3205 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3207 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3208 $line_item->{'amount'};
3209 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3210 $line_item->{'unit_amount'};
3211 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3213 $detail->{'sdate'} = $line_item->{'sdate'};
3214 $detail->{'edate'} = $line_item->{'edate'};
3215 $detail->{'seconds'} = $line_item->{'seconds'};
3216 $detail->{'svc_label'} = $line_item->{'svc_label'};
3218 push @detail_items, $detail;
3219 push @buf, ( [ $detail->{'description'},
3220 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3222 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3226 if ( $section->{'description'} ) {
3227 push @buf, ( ['','-----------'],
3228 [ $section->{'description'}. ' sub-total',
3229 $section->{'subtotal'} # already formatted this
3238 $invoice_data{current_less_finance} =
3239 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3241 # create a major section for previous balance if we have major sections,
3242 # or if previous_section is in summary form
3243 if ( ( $multisection && $self->enable_previous )
3244 || $conf->exists('previous_balance-summary_only') )
3246 unshift @sections, $previous_section if $pr_total;
3249 warn "$me adding taxes\n"
3252 foreach my $tax ( $self->_items_tax ) {
3254 $taxtotal += $tax->{'amount'};
3256 my $description = &$escape_function( $tax->{'description'} );
3257 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3259 if ( $multisection ) {
3261 my $money = $old_latex ? '' : $money_char;
3262 push @detail_items, {
3263 ext_description => [],
3266 description => $description,
3267 amount => $money. $amount,
3269 section => $tax_section,
3274 push @total_items, {
3275 'total_item' => $description,
3276 'total_amount' => $other_money_char. $amount,
3281 push @buf,[ $description,
3282 $money_char. $amount,
3289 $total->{'total_item'} = $self->mt('Sub-total');
3290 $total->{'total_amount'} =
3291 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3293 if ( $multisection ) {
3294 $tax_section->{'subtotal'} = $other_money_char.
3295 sprintf('%.2f', $taxtotal);
3296 $tax_section->{'pretotal'} = 'New charges sub-total '.
3297 $total->{'total_amount'};
3298 push @sections, $tax_section if $taxtotal;
3300 unshift @total_items, $total;
3303 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3305 push @buf,['','-----------'];
3306 push @buf,[$self->mt(
3307 (!$self->enable_previous)
3309 : 'Total New Charges'
3311 $money_char. sprintf("%10.2f",$self->charged) ];
3314 # calculate total, possibly including total owed on previous
3319 $item = $conf->config('previous_balance-exclude_from_total')
3320 || 'Total New Charges'
3321 if $conf->exists('previous_balance-exclude_from_total');
3322 my $amount = $self->charged;
3323 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3324 $amount += $pr_total;
3327 $total->{'total_item'} = &$embolden_function($self->mt($item));
3328 $total->{'total_amount'} =
3329 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3330 if ( $multisection ) {
3331 if ( $adjust_section->{'sort_weight'} ) {
3332 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3333 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3335 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3336 $other_money_char. sprintf('%.2f', $self->charged );
3339 push @total_items, $total;
3341 push @buf,['','-----------'];
3344 sprintf( '%10.2f', $amount )
3349 # if we're showing previous invoices, also show previous
3350 # credits and payments
3351 if ( $self->enable_previous
3352 and $self->can('_items_credits')
3353 and $self->can('_items_payments') )
3355 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3358 my $credittotal = 0;
3359 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3362 $total->{'total_item'} = &$escape_function($credit->{'description'});
3363 $credittotal += $credit->{'amount'};
3364 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3365 $adjusttotal += $credit->{'amount'};
3366 if ( $multisection ) {
3367 my $money = $old_latex ? '' : $money_char;
3368 push @detail_items, {
3369 ext_description => [],
3372 description => &$escape_function($credit->{'description'}),
3373 amount => $money. $credit->{'amount'},
3375 section => $adjust_section,
3378 push @total_items, $total;
3382 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3385 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3386 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3390 my $paymenttotal = 0;
3391 foreach my $payment ( $self->_items_payments ) {
3393 $total->{'total_item'} = &$escape_function($payment->{'description'});
3394 $paymenttotal += $payment->{'amount'};
3395 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3396 $adjusttotal += $payment->{'amount'};
3397 if ( $multisection ) {
3398 my $money = $old_latex ? '' : $money_char;
3399 push @detail_items, {
3400 ext_description => [],
3403 description => &$escape_function($payment->{'description'}),
3404 amount => $money. $payment->{'amount'},
3406 section => $adjust_section,
3409 push @total_items, $total;
3411 push @buf, [ $payment->{'description'},
3412 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3415 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3417 if ( $multisection ) {
3418 $adjust_section->{'subtotal'} = $other_money_char.
3419 sprintf('%.2f', $adjusttotal);
3420 push @sections, $adjust_section
3421 unless $adjust_section->{sort_weight};
3424 # create Balance Due message
3427 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3428 $total->{'total_amount'} =
3429 &$embolden_function(
3430 $other_money_char. sprintf('%.2f', #why? $summarypage
3431 # ? $self->charged +
3432 # $self->billing_balance
3434 $self->owed + $pr_total
3437 if ( $multisection && !$adjust_section->{sort_weight} ) {
3438 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3439 $total->{'total_amount'};
3441 push @total_items, $total;
3443 push @buf,['','-----------'];
3444 push @buf,[$self->balance_due_msg, $money_char.
3445 sprintf("%10.2f", $balance_due ) ];
3448 if ( $conf->exists('previous_balance-show_credit')
3449 and $cust_main->balance < 0 ) {
3450 my $credit_total = {
3451 'total_item' => &$embolden_function($self->credit_balance_msg),
3452 'total_amount' => &$embolden_function(
3453 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3456 if ( $multisection ) {
3457 $adjust_section->{'posttotal'} .= $newline_token .
3458 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3461 push @total_items, $credit_total;
3463 push @buf,['','-----------'];
3464 push @buf,[$self->credit_balance_msg, $money_char.
3465 sprintf("%10.2f", -$cust_main->balance ) ];
3469 if ( $multisection ) {
3470 if ($conf->exists('svc_phone_sections')) {
3472 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3473 $total->{'total_amount'} =
3474 &$embolden_function(
3475 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3477 my $last_section = pop @sections;
3478 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3479 $total->{'total_amount'};
3480 push @sections, $last_section;
3482 push @sections, @$late_sections
3486 # make a discounts-available section, even without multisection
3487 if ( $conf->exists('discount-show_available')
3488 and my @discounts_avail = $self->_items_discounts_avail ) {
3489 my $discount_section = {
3490 'description' => $self->mt('Discounts Available'),
3495 push @sections, $discount_section;
3496 push @detail_items, map { +{
3497 'ref' => '', #should this be something else?
3498 'section' => $discount_section,
3499 'description' => &$escape_function( $_->{description} ),
3500 'amount' => $money_char . &$escape_function( $_->{amount} ),
3501 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3502 } } @discounts_avail;
3505 # debugging hook: call this with 'diag' => 1 to just get a hash of
3506 # the invoice variables
3507 return \%invoice_data if ( $params{'diag'} );
3509 # All sections and items are built; now fill in templates.
3510 my @includelist = ();
3511 push @includelist, 'summary' if $summarypage;
3512 foreach my $include ( @includelist ) {
3514 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3517 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3519 @inc_src = $conf->config($inc_file, $agentnum);
3523 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3525 my $convert_map = $convert_maps{$format}{$include};
3527 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3528 s/--\@\]/$delimiters{$format}[1]/g;
3531 &$convert_map( $conf->config($inc_file, $agentnum) );
3535 my $inc_tt = new Text::Template (
3537 SOURCE => [ map "$_\n", @inc_src ],
3538 DELIMITERS => $delimiters{$format},
3539 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3541 unless ( $inc_tt->compile() ) {
3542 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3543 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3547 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3549 $invoice_data{$include} =~ s/\n+$//
3550 if ($format eq 'latex');
3555 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3556 /invoice_lines\((\d*)\)/;
3557 $invoice_lines += $1 || scalar(@buf);
3560 die "no invoice_lines() functions in template?"
3561 if ( $format eq 'template' && !$wasfunc );
3563 if ($format eq 'template') {
3565 if ( $invoice_lines ) {
3566 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3567 $invoice_data{'total_pages'}++
3568 if scalar(@buf) % $invoice_lines;
3571 #setup subroutine for the template
3572 $invoice_data{invoice_lines} = sub {
3573 my $lines = shift || scalar(@buf);
3585 push @collect, split("\n",
3586 $text_template->fill_in( HASH => \%invoice_data )
3588 $invoice_data{'page'}++;
3590 map "$_\n", @collect;
3592 # this is where we actually create the invoice
3593 warn "filling in template for invoice ". $self->invnum. "\n"
3595 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3598 $text_template->fill_in(HASH => \%invoice_data);
3602 # helper routine for generating date ranges
3603 sub _prior_month30s {
3606 [ 1, 2592000 ], # 0-30 days ago
3607 [ 2592000, 5184000 ], # 30-60 days ago
3608 [ 5184000, 7776000 ], # 60-90 days ago
3609 [ 7776000, 0 ], # 90+ days ago
3612 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3613 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3618 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3620 Returns an postscript invoice, as a scalar.
3622 Options can be passed as a hashref (recommended) or as a list of time, template
3623 and then any key/value pairs for any other options.
3625 I<time> an optional value used to control the printing of overdue messages. The
3626 default is now. It isn't the date of the invoice; that's the `_date' field.
3627 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3628 L<Time::Local> and L<Date::Parse> for conversion functions.
3630 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3637 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3638 my $ps = generate_ps($file);
3640 unlink($barcodefile) if $barcodefile;
3645 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3647 Returns an PDF invoice, as a scalar.
3649 Options can be passed as a hashref (recommended) or as a list of time, template
3650 and then any key/value pairs for any other options.
3652 I<time> an optional value used to control the printing of overdue messages. The
3653 default is now. It isn't the date of the invoice; that's the `_date' field.
3654 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3655 L<Time::Local> and L<Date::Parse> for conversion functions.
3657 I<template>, if specified, is the name of a suffix for alternate invoices.
3659 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3666 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3667 my $pdf = generate_pdf($file);
3669 unlink($barcodefile) if $barcodefile;
3674 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3676 Returns an HTML invoice, as a scalar.
3678 I<time> an optional value used to control the printing of overdue messages. The
3679 default is now. It isn't the date of the invoice; that's the `_date' field.
3680 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3681 L<Time::Local> and L<Date::Parse> for conversion functions.
3683 I<template>, if specified, is the name of a suffix for alternate invoices.
3685 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3687 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3688 when emailing the invoice as part of a multipart/related MIME email.
3696 %params = %{ shift() };
3698 $params{'time'} = shift;
3699 $params{'template'} = shift;
3700 $params{'cid'} = shift;
3703 $params{'format'} = 'html';
3705 $self->print_generic( %params );
3708 # quick subroutine for print_latex
3710 # There are ten characters that LaTeX treats as special characters, which
3711 # means that they do not simply typeset themselves:
3712 # # $ % & ~ _ ^ \ { }
3714 # TeX ignores blanks following an escaped character; if you want a blank (as
3715 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3719 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3720 $value =~ s/([<>])/\$$1\$/g;
3726 encode_entities($value);
3730 sub _html_escape_nbsp {
3731 my $value = _html_escape(shift);
3732 $value =~ s/ +/ /g;
3736 #utility methods for print_*
3738 sub _translate_old_latex_format {
3739 warn "_translate_old_latex_format called\n"
3746 if ( $line =~ /^%%Detail\s*$/ ) {
3748 push @template, q![@--!,
3749 q! foreach my $_tr_line (@detail_items) {!,
3750 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3751 q! $_tr_line->{'description'} .= !,
3752 q! "\\tabularnewline\n~~".!,
3753 q! join( "\\tabularnewline\n~~",!,
3754 q! @{$_tr_line->{'ext_description'}}!,
3758 while ( ( my $line_item_line = shift )
3759 !~ /^%%EndDetail\s*$/ ) {
3760 $line_item_line =~ s/'/\\'/g; # nice LTS
3761 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3762 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3763 push @template, " \$OUT .= '$line_item_line';";
3766 push @template, '}',
3769 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3771 push @template, '[@--',
3772 ' foreach my $_tr_line (@total_items) {';
3774 while ( ( my $total_item_line = shift )
3775 !~ /^%%EndTotalDetails\s*$/ ) {
3776 $total_item_line =~ s/'/\\'/g; # nice LTS
3777 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3778 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3779 push @template, " \$OUT .= '$total_item_line';";
3782 push @template, '}',
3786 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3787 push @template, $line;
3793 warn "$_\n" foreach @template;
3801 my $conf = $self->conf;
3803 #check for an invoice-specific override
3804 return $self->invoice_terms if $self->invoice_terms;
3806 #check for a customer- specific override
3807 my $cust_main = $self->cust_main;
3808 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3810 #use configured default
3811 $conf->config('invoice_default_terms') || '';
3817 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3818 $duedate = $self->_date() + ( $1 * 86400 );
3825 $self->due_date ? time2str(shift, $self->due_date) : '';
3828 sub balance_due_msg {
3830 my $msg = $self->mt('Balance Due');
3831 return $msg unless $self->terms;
3832 if ( $self->due_date ) {
3833 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3834 $self->due_date2str($date_format);
3835 } elsif ( $self->terms ) {
3836 $msg .= ' - '. $self->terms;
3841 sub balance_due_date {
3843 my $conf = $self->conf;
3845 if ( $conf->exists('invoice_default_terms')
3846 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3847 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3852 sub credit_balance_msg {
3854 $self->mt('Credit Balance Remaining')
3857 =item invnum_date_pretty
3859 Returns a string with the invoice number and date, for example:
3860 "Invoice #54 (3/20/2008)"
3864 sub invnum_date_pretty {
3866 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3871 Returns a string with the date, for example: "3/20/2008"
3877 time2str($date_format, $self->_date);
3880 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3882 Generate section information for all items appearing on this invoice.
3883 This will only be called for multi-section invoices.
3885 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3886 related display records (L<FS::cust_bill_pkg_display>) and organize
3887 them into two groups ("early" and "late" according to whether they come
3888 before or after the total), then into sections. A subtotal is calculated
3891 Section descriptions are returned in sort weight order. Each consists
3892 of a hash containing:
3894 description: the package category name, escaped
3895 subtotal: the total charges in that section
3896 tax_section: a flag indicating that the section contains only tax charges
3897 summarized: same as tax_section, for some reason
3898 sort_weight: the package category's sort weight
3900 If 'condense' is set on the display record, it also contains everything
3901 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3902 coderefs to generate parts of the invoice. This is not advised.
3906 LATE: an arrayref to push the "late" section hashes onto. The "early"
3907 group is simply returned from the method.
3909 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3910 Turning this on has the following effects:
3911 - Ignores display items with the 'summary' flag.
3912 - Combines all items into the "early" group.
3913 - Creates sections for all non-disabled package categories, even if they
3914 have no charges on this invoice, as well as a section with no name.
3916 ESCAPE: an escape function to use for section titles.
3918 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3919 sorted list. If there are any of these, section subtotals exclude
3922 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3923 passed through to C<_condense_section()>.
3927 use vars qw(%pkg_category_cache);
3928 sub _items_sections {
3931 my $summarypage = shift;
3933 my $extra_sections = shift;
3937 my %late_subtotal = ();
3940 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3943 my $usage = $cust_bill_pkg->usage;
3945 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3946 next if ( $display->summary && $summarypage );
3948 my $section = $display->section;
3949 my $type = $display->type;
3951 $not_tax{$section} = 1
3952 unless $cust_bill_pkg->pkgnum == 0;
3954 if ( $display->post_total && !$summarypage ) {
3955 if (! $type || $type eq 'S') {
3956 $late_subtotal{$section} += $cust_bill_pkg->setup
3957 if $cust_bill_pkg->setup != 0
3958 || $cust_bill_pkg->setup_show_zero;
3962 $late_subtotal{$section} += $cust_bill_pkg->recur
3963 if $cust_bill_pkg->recur != 0
3964 || $cust_bill_pkg->recur_show_zero;
3967 if ($type && $type eq 'R') {
3968 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3969 if $cust_bill_pkg->recur != 0
3970 || $cust_bill_pkg->recur_show_zero;
3973 if ($type && $type eq 'U') {
3974 $late_subtotal{$section} += $usage
3975 unless scalar(@$extra_sections);
3980 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3982 if (! $type || $type eq 'S') {
3983 $subtotal{$section} += $cust_bill_pkg->setup
3984 if $cust_bill_pkg->setup != 0
3985 || $cust_bill_pkg->setup_show_zero;
3989 $subtotal{$section} += $cust_bill_pkg->recur
3990 if $cust_bill_pkg->recur != 0
3991 || $cust_bill_pkg->recur_show_zero;
3994 if ($type && $type eq 'R') {
3995 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3996 if $cust_bill_pkg->recur != 0
3997 || $cust_bill_pkg->recur_show_zero;
4000 if ($type && $type eq 'U') {
4001 $subtotal{$section} += $usage
4002 unless scalar(@$extra_sections);
4011 %pkg_category_cache = ();
4013 push @$late, map { { 'description' => &{$escape}($_),
4014 'subtotal' => $late_subtotal{$_},
4016 'sort_weight' => ( _pkg_category($_)
4017 ? _pkg_category($_)->weight
4020 ((_pkg_category($_) && _pkg_category($_)->condense)
4021 ? $self->_condense_section($format)
4025 sort _sectionsort keys %late_subtotal;
4028 if ( $summarypage ) {
4029 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4030 map { $_->categoryname } qsearch('pkg_category', {});
4031 push @sections, '' if exists($subtotal{''});
4033 @sections = keys %subtotal;
4036 my @early = map { { 'description' => &{$escape}($_),
4037 'subtotal' => $subtotal{$_},
4038 'summarized' => $not_tax{$_} ? '' : 'Y',
4039 'tax_section' => $not_tax{$_} ? '' : 'Y',
4040 'sort_weight' => ( _pkg_category($_)
4041 ? _pkg_category($_)->weight
4044 ((_pkg_category($_) && _pkg_category($_)->condense)
4045 ? $self->_condense_section($format)
4050 push @early, @$extra_sections if $extra_sections;
4052 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4056 #helper subs for above
4059 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4063 my $categoryname = shift;
4064 $pkg_category_cache{$categoryname} ||=
4065 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4068 my %condensed_format = (
4069 'label' => [ qw( Description Qty Amount ) ],
4071 sub { shift->{description} },
4072 sub { shift->{quantity} },
4073 sub { my($href, %opt) = @_;
4074 ($opt{dollar} || ''). $href->{amount};
4077 'align' => [ qw( l r r ) ],
4078 'span' => [ qw( 5 1 1 ) ], # unitprices?
4079 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4082 sub _condense_section {
4083 my ( $self, $format ) = ( shift, shift );
4085 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4086 qw( description_generator
4089 total_line_generator
4094 sub _condensed_generator_defaults {
4095 my ( $self, $format ) = ( shift, shift );
4096 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4105 sub _condensed_header_generator {
4106 my ( $self, $format ) = ( shift, shift );
4108 my ( $f, $prefix, $suffix, $separator, $column ) =
4109 _condensed_generator_defaults($format);
4111 if ($format eq 'latex') {
4112 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4113 $suffix = "\\\\\n\\hline";
4116 sub { my ($d,$a,$s,$w) = @_;
4117 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4119 } elsif ( $format eq 'html' ) {
4120 $prefix = '<th></th>';
4124 sub { my ($d,$a,$s,$w) = @_;
4125 return qq!<th align="$html_align{$a}">$d</th>!;
4133 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4135 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4138 $prefix. join($separator, @result). $suffix;
4143 sub _condensed_description_generator {
4144 my ( $self, $format ) = ( shift, shift );
4146 my ( $f, $prefix, $suffix, $separator, $column ) =
4147 _condensed_generator_defaults($format);
4149 my $money_char = '$';
4150 if ($format eq 'latex') {
4151 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4153 $separator = " & \n";
4155 sub { my ($d,$a,$s,$w) = @_;
4156 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4158 $money_char = '\\dollar';
4159 }elsif ( $format eq 'html' ) {
4160 $prefix = '"><td align="center"></td>';
4164 sub { my ($d,$a,$s,$w) = @_;
4165 return qq!<td align="$html_align{$a}">$d</td>!;
4167 #$money_char = $conf->config('money_char') || '$';
4168 $money_char = ''; # this is madness
4176 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4178 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4180 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4181 map { $f->{$_}->[$i] } qw(align span width)
4185 $prefix. join( $separator, @result ). $suffix;
4190 sub _condensed_total_generator {
4191 my ( $self, $format ) = ( shift, shift );
4193 my ( $f, $prefix, $suffix, $separator, $column ) =
4194 _condensed_generator_defaults($format);
4197 if ($format eq 'latex') {
4200 $separator = " & \n";
4202 sub { my ($d,$a,$s,$w) = @_;
4203 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4205 }elsif ( $format eq 'html' ) {
4209 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4211 sub { my ($d,$a,$s,$w) = @_;
4212 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4221 # my $r = &{$f->{fields}->[$i]}(@args);
4222 # $r .= ' Total' unless $i;
4224 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4226 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4227 map { $f->{$_}->[$i] } qw(align span width)
4231 $prefix. join( $separator, @result ). $suffix;
4236 =item total_line_generator FORMAT
4238 Returns a coderef used for generation of invoice total line items for this
4239 usage_class. FORMAT is either html or latex
4243 # should not be used: will have issues with hash element names (description vs
4244 # total_item and amount vs total_amount -- another array of functions?
4246 sub _condensed_total_line_generator {
4247 my ( $self, $format ) = ( shift, shift );
4249 my ( $f, $prefix, $suffix, $separator, $column ) =
4250 _condensed_generator_defaults($format);
4253 if ($format eq 'latex') {
4256 $separator = " & \n";
4258 sub { my ($d,$a,$s,$w) = @_;
4259 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4261 }elsif ( $format eq 'html' ) {
4265 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4267 sub { my ($d,$a,$s,$w) = @_;
4268 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4277 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4279 &{$column}( &{$f->{fields}->[$i]}(@args),
4280 map { $f->{$_}->[$i] } qw(align span width)
4284 $prefix. join( $separator, @result ). $suffix;
4289 #sub _items_extra_usage_sections {
4291 # my $escape = shift;
4293 # my %sections = ();
4295 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4296 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4298 # next unless $cust_bill_pkg->pkgnum > 0;
4300 # foreach my $section ( keys %usage_class ) {
4302 # my $usage = $cust_bill_pkg->usage($section);
4304 # next unless $usage && $usage > 0;
4306 # $sections{$section} ||= 0;
4307 # $sections{$section} += $usage;
4313 # map { { 'description' => &{$escape}($_),
4314 # 'subtotal' => $sections{$_},
4315 # 'summarized' => '',
4316 # 'tax_section' => '',
4319 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4323 sub _items_extra_usage_sections {
4325 my $conf = $self->conf;
4333 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4335 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4336 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4337 next unless $cust_bill_pkg->pkgnum > 0;
4339 foreach my $classnum ( keys %usage_class ) {
4340 my $section = $usage_class{$classnum}->classname;
4341 $classnums{$section} = $classnum;
4343 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4344 my $amount = $detail->amount;
4345 next unless $amount && $amount > 0;
4347 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4348 $sections{$section}{amount} += $amount; #subtotal
4349 $sections{$section}{calls}++;
4350 $sections{$section}{duration} += $detail->duration;
4352 my $desc = $detail->regionname;
4353 my $description = $desc;
4354 $description = substr($desc, 0, $maxlength). '...'
4355 if $format eq 'latex' && length($desc) > $maxlength;
4357 $lines{$section}{$desc} ||= {
4358 description => &{$escape}($description),
4359 #pkgpart => $part_pkg->pkgpart,
4360 pkgnum => $cust_bill_pkg->pkgnum,
4365 #unit_amount => $cust_bill_pkg->unitrecur,
4366 quantity => $cust_bill_pkg->quantity,
4367 product_code => 'N/A',
4368 ext_description => [],
4371 $lines{$section}{$desc}{amount} += $amount;
4372 $lines{$section}{$desc}{calls}++;
4373 $lines{$section}{$desc}{duration} += $detail->duration;
4379 my %sectionmap = ();
4380 foreach (keys %sections) {
4381 my $usage_class = $usage_class{$classnums{$_}};
4382 $sectionmap{$_} = { 'description' => &{$escape}($_),
4383 'amount' => $sections{$_}{amount}, #subtotal
4384 'calls' => $sections{$_}{calls},
4385 'duration' => $sections{$_}{duration},
4387 'tax_section' => '',
4388 'sort_weight' => $usage_class->weight,
4389 ( $usage_class->format
4390 ? ( map { $_ => $usage_class->$_($format) }
4391 qw( description_generator header_generator total_generator total_line_generator )
4398 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4402 foreach my $section ( keys %lines ) {
4403 foreach my $line ( keys %{$lines{$section}} ) {
4404 my $l = $lines{$section}{$line};
4405 $l->{section} = $sectionmap{$section};
4406 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4407 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4412 return(\@sections, \@lines);
4418 my $end = $self->_date;
4420 # start at date of previous invoice + 1 second or 0 if no previous invoice
4421 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4422 $start = 0 if !$start;
4425 my $cust_main = $self->cust_main;
4426 my @pkgs = $cust_main->all_pkgs;
4427 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4430 foreach my $pkg ( @pkgs ) {
4431 my @h_cust_svc = $pkg->h_cust_svc($end);
4432 foreach my $h_cust_svc ( @h_cust_svc ) {
4433 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4434 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4436 my $inserted = $h_cust_svc->date_inserted;
4437 my $deleted = $h_cust_svc->date_deleted;
4438 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4440 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4442 # DID either activated or ported in; cannot be both for same DID simultaneously
4443 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4444 && (!$phone_inserted->lnp_status
4445 || $phone_inserted->lnp_status eq ''
4446 || $phone_inserted->lnp_status eq 'native')) {
4449 else { # this one not so clean, should probably move to (h_)svc_phone
4450 my $phone_portedin = qsearchs( 'h_svc_phone',
4451 { 'svcnum' => $h_cust_svc->svcnum,
4452 'lnp_status' => 'portedin' },
4453 FS::h_svc_phone->sql_h_searchs($end),
4455 $num_portedin++ if $phone_portedin;
4458 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4459 if($deleted >= $start && $deleted <= $end && $phone_deleted
4460 && (!$phone_deleted->lnp_status
4461 || $phone_deleted->lnp_status ne 'portingout')) {
4464 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4465 && $phone_deleted->lnp_status
4466 && $phone_deleted->lnp_status eq 'portingout') {
4470 # increment usage minutes
4471 if ( $phone_inserted ) {
4472 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4473 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4476 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4479 # don't look at this service again
4480 push @seen, $h_cust_svc->svcnum;
4484 $minutes = sprintf("%d", $minutes);
4485 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4486 . "$num_deactivated Ported-Out: $num_portedout ",
4487 "Total Minutes: $minutes");
4490 sub _items_accountcode_cdr {
4495 my $section = { 'amount' => 0,
4498 'sort_weight' => '',
4500 'description' => 'Usage by Account Code',
4506 my %accountcodes = ();
4508 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4509 next unless $cust_bill_pkg->pkgnum > 0;
4511 my @header = $cust_bill_pkg->details_header;
4512 next unless scalar(@header);
4513 $section->{'header'} = join(',',@header);
4515 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4517 $section->{'header'} = $detail->formatted('format' => $format)
4518 if($detail->detail eq $section->{'header'});
4520 my $accountcode = $detail->accountcode;
4521 next unless $accountcode;
4523 my $amount = $detail->amount;
4524 next unless $amount && $amount > 0;
4526 $accountcodes{$accountcode} ||= {
4527 description => $accountcode,
4534 product_code => 'N/A',
4535 section => $section,
4536 ext_description => [ $section->{'header'} ],
4540 $section->{'amount'} += $amount;
4541 $accountcodes{$accountcode}{'amount'} += $amount;
4542 $accountcodes{$accountcode}{calls}++;
4543 $accountcodes{$accountcode}{duration} += $detail->duration;
4544 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4548 foreach my $l ( values %accountcodes ) {
4549 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4550 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4551 foreach my $sorted_detail ( @sorted_detail ) {
4552 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4554 delete $l->{detail_temp};
4558 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4560 return ($section,\@sorted_lines);
4563 sub _items_svc_phone_sections {
4565 my $conf = $self->conf;
4573 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4575 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4576 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4578 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4579 next unless $cust_bill_pkg->pkgnum > 0;
4581 my @header = $cust_bill_pkg->details_header;
4582 next unless scalar(@header);
4584 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4586 my $phonenum = $detail->phonenum;
4587 next unless $phonenum;
4589 my $amount = $detail->amount;
4590 next unless $amount && $amount > 0;
4592 $sections{$phonenum} ||= { 'amount' => 0,
4595 'sort_weight' => -1,
4596 'phonenum' => $phonenum,
4598 $sections{$phonenum}{amount} += $amount; #subtotal
4599 $sections{$phonenum}{calls}++;
4600 $sections{$phonenum}{duration} += $detail->duration;
4602 my $desc = $detail->regionname;
4603 my $description = $desc;
4604 $description = substr($desc, 0, $maxlength). '...'
4605 if $format eq 'latex' && length($desc) > $maxlength;
4607 $lines{$phonenum}{$desc} ||= {
4608 description => &{$escape}($description),
4609 #pkgpart => $part_pkg->pkgpart,
4617 product_code => 'N/A',
4618 ext_description => [],
4621 $lines{$phonenum}{$desc}{amount} += $amount;
4622 $lines{$phonenum}{$desc}{calls}++;
4623 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4625 my $line = $usage_class{$detail->classnum}->classname;
4626 $sections{"$phonenum $line"} ||=
4630 'sort_weight' => $usage_class{$detail->classnum}->weight,
4631 'phonenum' => $phonenum,
4632 'header' => [ @header ],
4634 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4635 $sections{"$phonenum $line"}{calls}++;
4636 $sections{"$phonenum $line"}{duration} += $detail->duration;
4638 $lines{"$phonenum $line"}{$desc} ||= {
4639 description => &{$escape}($description),
4640 #pkgpart => $part_pkg->pkgpart,
4648 product_code => 'N/A',
4649 ext_description => [],
4652 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4653 $lines{"$phonenum $line"}{$desc}{calls}++;
4654 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4655 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4656 $detail->formatted('format' => $format);
4661 my %sectionmap = ();
4662 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4663 foreach ( keys %sections ) {
4664 my @header = @{ $sections{$_}{header} || [] };
4666 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4667 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4668 my $usage_class = $summary ? $simple : $usage_simple;
4669 my $ending = $summary ? ' usage charges' : '';
4672 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4674 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4675 'amount' => $sections{$_}{amount}, #subtotal
4676 'calls' => $sections{$_}{calls},
4677 'duration' => $sections{$_}{duration},
4679 'tax_section' => '',
4680 'phonenum' => $sections{$_}{phonenum},
4681 'sort_weight' => $sections{$_}{sort_weight},
4682 'post_total' => $summary, #inspire pagebreak
4684 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4685 qw( description_generator
4688 total_line_generator
4695 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4696 $a->{sort_weight} <=> $b->{sort_weight}
4701 foreach my $section ( keys %lines ) {
4702 foreach my $line ( keys %{$lines{$section}} ) {
4703 my $l = $lines{$section}{$line};
4704 $l->{section} = $sectionmap{$section};
4705 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4706 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4711 if($conf->exists('phone_usage_class_summary')) {
4712 # this only works with Latex
4716 # after this, we'll have only two sections per DID:
4717 # Calls Summary and Calls Detail
4718 foreach my $section ( @sections ) {
4719 if($section->{'post_total'}) {
4720 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4721 $section->{'total_line_generator'} = sub { '' };
4722 $section->{'total_generator'} = sub { '' };
4723 $section->{'header_generator'} = sub { '' };
4724 $section->{'description_generator'} = '';
4725 push @newsections, $section;
4726 my %calls_detail = %$section;
4727 $calls_detail{'post_total'} = '';
4728 $calls_detail{'sort_weight'} = '';
4729 $calls_detail{'description_generator'} = sub { '' };
4730 $calls_detail{'header_generator'} = sub {
4731 return ' & Date/Time & Called Number & Duration & Price'
4732 if $format eq 'latex';
4735 $calls_detail{'description'} = 'Calls Detail: '
4736 . $section->{'phonenum'};
4737 push @newsections, \%calls_detail;
4741 # after this, each usage class is collapsed/summarized into a single
4742 # line under the Calls Summary section
4743 foreach my $newsection ( @newsections ) {
4744 if($newsection->{'post_total'}) { # this means Calls Summary
4745 foreach my $section ( @sections ) {
4746 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4747 && !$section->{'post_total'});
4748 my $newdesc = $section->{'description'};
4749 my $tn = $section->{'phonenum'};
4750 $newdesc =~ s/$tn//g;
4751 my $line = { ext_description => [],
4755 calls => $section->{'calls'},
4756 section => $newsection,
4757 duration => $section->{'duration'},
4758 description => $newdesc,
4759 amount => sprintf("%.2f",$section->{'amount'}),
4760 product_code => 'N/A',
4762 push @newlines, $line;
4767 # after this, Calls Details is populated with all CDRs
4768 foreach my $newsection ( @newsections ) {
4769 if(!$newsection->{'post_total'}) { # this means Calls Details
4770 foreach my $line ( @lines ) {
4771 next unless (scalar(@{$line->{'ext_description'}}) &&
4772 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4774 my @extdesc = @{$line->{'ext_description'}};
4776 foreach my $extdesc ( @extdesc ) {
4777 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4778 push @newextdesc, $extdesc;
4780 $line->{'ext_description'} = \@newextdesc;
4781 $line->{'section'} = $newsection;
4782 push @newlines, $line;
4787 return(\@newsections, \@newlines);
4790 return(\@sections, \@lines);
4794 sub _items { # seems to be unused
4797 #my @display = scalar(@_)
4799 # : qw( _items_previous _items_pkg );
4800 # #: qw( _items_pkg );
4801 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4802 my @display = qw( _items_previous _items_pkg );
4805 foreach my $display ( @display ) {
4806 push @b, $self->$display(@_);
4811 sub _items_previous {
4813 my $conf = $self->conf;
4814 my $cust_main = $self->cust_main;
4815 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4817 foreach ( @pr_cust_bill ) {
4818 my $date = $conf->exists('invoice_show_prior_due_date')
4819 ? 'due '. $_->due_date2str($date_format)
4820 : time2str($date_format, $_->_date);
4822 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4823 #'pkgpart' => 'N/A',
4825 'amount' => sprintf("%.2f", $_->owed),
4831 # 'description' => 'Previous Balance',
4832 # #'pkgpart' => 'N/A',
4833 # 'pkgnum' => 'N/A',
4834 # 'amount' => sprintf("%10.2f", $pr_total ),
4835 # 'ext_description' => [ map {
4836 # "Invoice ". $_->invnum.
4837 # " (". time2str("%x",$_->_date). ") ".
4838 # sprintf("%10.2f", $_->owed)
4839 # } @pr_cust_bill ],
4844 =item _items_pkg [ OPTIONS ]
4846 Return line item hashes for each package item on this invoice. Nearly
4849 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4851 The only OPTIONS accepted is 'section', which may point to a hashref
4852 with a key named 'condensed', which may have a true value. If it
4853 does, this method tries to merge identical items into items with
4854 'quantity' equal to the number of items (not the sum of their
4855 separate quantities, for some reason).
4863 warn "$me _items_pkg searching for all package line items\n"
4866 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4868 warn "$me _items_pkg filtering line items\n"
4870 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4872 if ($options{section} && $options{section}->{condensed}) {
4874 warn "$me _items_pkg condensing section\n"
4878 local $Storable::canonical = 1;
4879 foreach ( @items ) {
4881 delete $item->{ref};
4882 delete $item->{ext_description};
4883 my $key = freeze($item);
4884 $itemshash{$key} ||= 0;
4885 $itemshash{$key} ++; # += $item->{quantity};
4887 @items = sort { $a->{description} cmp $b->{description} }
4888 map { my $i = thaw($_);
4889 $i->{quantity} = $itemshash{$_};
4891 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4897 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4904 return 0 unless $a->itemdesc cmp $b->itemdesc;
4905 return -1 if $b->itemdesc eq 'Tax';
4906 return 1 if $a->itemdesc eq 'Tax';
4907 return -1 if $b->itemdesc eq 'Other surcharges';
4908 return 1 if $a->itemdesc eq 'Other surcharges';
4909 $a->itemdesc cmp $b->itemdesc;
4914 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4915 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4918 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4920 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4921 list of hashrefs describing the line items they generate on the invoice.
4923 OPTIONS may include:
4925 format: the invoice format.
4927 escape_function: the function used to escape strings.
4929 DEPRECATED? (expensive, mostly unused?)
4930 format_function: the function used to format CDRs.
4932 section: a hashref containing 'description'; if this is present,
4933 cust_bill_pkg_display records not belonging to this section are
4936 multisection: a flag indicating that this is a multisection invoice,
4937 which does something complicated.
4939 multilocation: a flag to display the location label for the package.
4941 Returns a list of hashrefs, each of which may contain:
4943 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4944 ext_description, which is an arrayref of detail lines to show below
4949 sub _items_cust_bill_pkg {
4951 my $conf = $self->conf;
4952 my $cust_bill_pkgs = shift;
4955 my $format = $opt{format} || '';
4956 my $escape_function = $opt{escape_function} || sub { shift };
4957 my $format_function = $opt{format_function} || '';
4958 my $no_usage = $opt{no_usage} || '';
4959 my $unsquelched = $opt{unsquelched} || ''; #unused
4960 my $section = $opt{section}->{description} if $opt{section};
4961 my $summary_page = $opt{summary_page} || ''; #unused
4962 my $multilocation = $opt{multilocation} || '';
4963 my $multisection = $opt{multisection} || '';
4964 my $discount_show_always = 0;
4966 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4968 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4971 my ($s, $r, $u) = ( undef, undef, undef );
4972 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4975 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4976 if ( $_ && !$cust_bill_pkg->hidden ) {
4977 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4978 $_->{amount} =~ s/^\-0\.00$/0.00/;
4979 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4981 if $_->{amount} != 0
4982 || $discount_show_always
4983 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4984 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4990 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4992 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4993 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4996 foreach my $display ( grep { defined($section)
4997 ? $_->section eq $section
5000 #grep { !$_->summary || !$summary_page } # bunk!
5001 grep { !$_->summary || $multisection }
5002 @cust_bill_pkg_display
5006 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
5007 $display->billpkgdisplaynum. "\n"
5010 my $type = $display->type;
5012 my $desc = $cust_bill_pkg->desc;
5013 $desc = substr($desc, 0, $maxlength). '...'
5014 if $format eq 'latex' && length($desc) > $maxlength;
5016 my %details_opt = ( 'format' => $format,
5017 'escape_function' => $escape_function,
5018 'format_function' => $format_function,
5019 'no_usage' => $opt{'no_usage'},
5022 if ( $cust_bill_pkg->pkgnum > 0 ) {
5024 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5027 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5029 # which pkgpart to show for display purposes?
5030 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5032 # start/end dates for invoice formats that do nonstandard
5034 my %item_dates = ();
5035 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5036 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5038 if ( (!$type || $type eq 'S')
5039 && ( $cust_bill_pkg->setup != 0
5040 || $cust_bill_pkg->setup_show_zero
5045 warn "$me _items_cust_bill_pkg adding setup\n"
5048 my $description = $desc;
5049 $description .= ' Setup'
5050 if $cust_bill_pkg->recur != 0
5051 || $discount_show_always
5052 || $cust_bill_pkg->recur_show_zero;
5056 unless ( $cust_pkg->part_pkg->hide_svc_detail
5057 || $cust_bill_pkg->hidden )
5060 my @svc_labels = map &{$escape_function}($_),
5061 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5062 push @d, @svc_labels
5063 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5064 $svc_label = $svc_labels[0];
5066 if ( $multilocation ) {
5067 my $loc = $cust_pkg->location_label;
5068 $loc = substr($loc, 0, $maxlength). '...'
5069 if $format eq 'latex' && length($loc) > $maxlength;
5070 push @d, &{$escape_function}($loc);
5073 } #unless hiding service details
5075 push @d, $cust_bill_pkg->details(%details_opt)
5076 if $cust_bill_pkg->recur == 0;
5078 if ( $cust_bill_pkg->hidden ) {
5079 $s->{amount} += $cust_bill_pkg->setup;
5080 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5081 push @{ $s->{ext_description} }, @d;
5085 description => $description,
5086 pkgpart => $pkgpart,
5087 pkgnum => $cust_bill_pkg->pkgnum,
5088 amount => $cust_bill_pkg->setup,
5089 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5090 unit_amount => $cust_bill_pkg->unitsetup,
5091 quantity => $cust_bill_pkg->quantity,
5092 ext_description => \@d,
5093 svc_label => ($svc_label || ''),
5099 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5101 $cust_bill_pkg->recur != 0
5102 || $cust_bill_pkg->setup == 0
5103 || $discount_show_always
5104 || $cust_bill_pkg->recur_show_zero
5109 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5112 my $is_summary = $display->summary;
5113 my $description = ($is_summary && $type && $type eq 'U')
5114 ? "Usage charges" : $desc;
5116 my $part_pkg = $cust_pkg->part_pkg;
5118 #pry be a bit more efficient to look some of this conf stuff up
5121 $conf->exists('disable_line_item_date_ranges')
5122 || $part_pkg->option('disable_line_item_date_ranges',1)
5123 || ! $cust_bill_pkg->sdate
5124 || ! $cust_bill_pkg->edate
5127 my $date_style = '';
5128 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5129 $cust_main->agentnum
5131 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5132 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5133 $cust_main->agentnum
5135 if ( defined($date_style) && $date_style eq 'month_of' ) {
5136 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5137 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5138 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5139 $cust_main->agentnum
5141 $desc .= ' ' unless $desc =~ /\s$/;
5142 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5144 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5145 " - ". time2str($date_format, $cust_bill_pkg->edate);
5147 $description .= " ($time_period)";
5151 my @seconds = (); # for display of usage info
5154 #at least until cust_bill_pkg has "past" ranges in addition to
5155 #the "future" sdate/edate ones... see #3032
5156 my @dates = ( $self->_date );
5157 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5158 push @dates, $prev->sdate if $prev;
5159 push @dates, undef if !$prev;
5161 unless ( $cust_pkg->part_pkg->hide_svc_detail
5162 || $cust_bill_pkg->itemdesc
5163 || $cust_bill_pkg->hidden
5164 || $is_summary && $type && $type eq 'U' )
5167 warn "$me _items_cust_bill_pkg adding service details\n"
5170 my @svc_labels = map &{$escape_function}($_),
5171 $cust_pkg->h_labels_short(@dates, 'I');
5172 push @d, @svc_labels
5173 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5174 $svc_label = $svc_labels[0];
5176 warn "$me _items_cust_bill_pkg done adding service details\n"
5179 if ( $multilocation ) {
5180 my $loc = $cust_pkg->location_label;
5181 $loc = substr($loc, 0, $maxlength). '...'
5182 if $format eq 'latex' && length($loc) > $maxlength;
5183 push @d, &{$escape_function}($loc);
5186 # Display of seconds_since_sqlradacct:
5187 # On the invoice, when processing @detail_items, look for a field
5188 # named 'seconds'. This will contain total seconds for each
5189 # service, in the same order as @ext_description. For services
5190 # that don't support this it will show undef.
5191 if ( $conf->exists('svc_acct-usage_seconds')
5192 and ! $cust_bill_pkg->pkgpart_override ) {
5193 foreach my $cust_svc (
5194 $cust_pkg->h_cust_svc(@dates, 'I')
5197 # eval because not having any part_export_usage exports
5198 # is a fatal error, last_bill/_date because that's how
5199 # sqlradius_hour billing does it
5201 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5203 push @seconds, $sec;
5205 } #if svc_acct-usage_seconds
5209 unless ( $is_summary ) {
5210 warn "$me _items_cust_bill_pkg adding details\n"
5213 #instead of omitting details entirely in this case (unwanted side
5214 # effects), just omit CDRs
5215 $details_opt{'no_usage'} = 1
5216 if $type && $type eq 'R';
5218 push @d, $cust_bill_pkg->details(%details_opt);
5221 warn "$me _items_cust_bill_pkg calculating amount\n"
5226 $amount = $cust_bill_pkg->recur;
5227 } elsif ($type eq 'R') {
5228 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5229 } elsif ($type eq 'U') {
5230 $amount = $cust_bill_pkg->usage;
5233 if ( !$type || $type eq 'R' ) {
5235 warn "$me _items_cust_bill_pkg adding recur\n"
5238 if ( $cust_bill_pkg->hidden ) {
5239 $r->{amount} += $amount;
5240 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5241 push @{ $r->{ext_description} }, @d;
5244 description => $description,
5245 pkgpart => $pkgpart,
5246 pkgnum => $cust_bill_pkg->pkgnum,
5248 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5249 unit_amount => $cust_bill_pkg->unitrecur,
5250 quantity => $cust_bill_pkg->quantity,
5252 ext_description => \@d,
5253 svc_label => ($svc_label || ''),
5255 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5258 } else { # $type eq 'U'
5260 warn "$me _items_cust_bill_pkg adding usage\n"
5263 if ( $cust_bill_pkg->hidden ) {
5264 $u->{amount} += $amount;
5265 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5266 push @{ $u->{ext_description} }, @d;
5269 description => $description,
5270 pkgpart => $pkgpart,
5271 pkgnum => $cust_bill_pkg->pkgnum,
5273 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5274 unit_amount => $cust_bill_pkg->unitrecur,
5275 quantity => $cust_bill_pkg->quantity,
5277 ext_description => \@d,
5282 } # recurring or usage with recurring charge
5284 } else { #pkgnum tax or one-shot line item (??)
5286 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5289 if ( $cust_bill_pkg->setup != 0 ) {
5291 'description' => $desc,
5292 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5295 if ( $cust_bill_pkg->recur != 0 ) {
5297 'description' => "$desc (".
5298 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5299 time2str($date_format, $cust_bill_pkg->edate). ')',
5300 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5308 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5309 && $conf->exists('discount-show-always'));
5313 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5315 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5316 $_->{amount} =~ s/^\-0\.00$/0.00/;
5317 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5319 if $_->{amount} != 0
5320 || $discount_show_always
5321 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5322 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5326 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5333 sub _items_credits {
5334 my( $self, %opt ) = @_;
5335 my $trim_len = $opt{'trim_len'} || 60;
5340 if ( $self->conf->exists('previous_balance-payments_since') ) {
5342 $date = $self->previous_bill->_date if $self->previous_bill;
5343 @objects = qsearch('cust_credit', {
5344 'custnum' => $self->custnum,
5345 '_date' => {op => '>=', value => $date},
5347 # hard to do this in the qsearch...
5348 @objects = grep { $_->_date < $self->_date } @objects;
5350 @objects = $self->cust_credited;
5353 foreach my $obj ( @objects ) {
5354 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5356 my $reason = substr($cust_credit->reason, 0, $trim_len);
5357 $reason .= '...' if length($reason) < length($cust_credit->reason);
5358 $reason = " ($reason) " if $reason;
5361 #'description' => 'Credit ref\#'. $_->crednum.
5362 # " (". time2str("%x",$_->cust_credit->_date) .")".
5364 'description' => $self->mt('Credit applied').' '.
5365 time2str($date_format,$obj->_date). $reason,
5366 'amount' => sprintf("%.2f",$obj->amount),
5374 sub _items_payments {
5378 my $detailed = $self->conf->exists('invoice_payment_details');
5380 if ( $self->conf->exists('previous_balance-payments_since') ) {
5382 $date = $self->previous_bill->_date if $self->previous_bill;
5383 @objects = qsearch('cust_pay', {
5384 'custnum' => $self->custnum,
5385 '_date' => {op => '>=', value => $date},
5387 @objects = grep { $_->_date < $self->_date } @objects;
5389 @objects = $self->cust_bill_pay;
5392 foreach my $obj (@objects) {
5393 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5394 my $desc = $self->mt('Payment received').' '.
5395 time2str($date_format, $cust_pay->_date );
5396 $desc .= $self->mt(' via ') .
5397 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5401 'description' => $desc,
5402 'amount' => sprintf("%.2f", $obj->amount )
5410 =item _items_discounts_avail
5412 Returns an array of line item hashrefs representing available term discounts
5413 for this invoice. This makes the same assumptions that apply to term
5414 discounts in general: that the package is billed monthly, at a flat rate,
5415 with no usage charges. A prorated first month will be handled, as will
5416 a setup fee if the discount is allowed to apply to setup fees.
5420 sub _items_discounts_avail {
5422 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5424 my %plans = $self->discount_plans;
5426 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5430 my $plan = $plans{$months};
5432 my $term_total = sprintf('%.2f', $plan->discounted_total);
5433 my $percent = sprintf('%.0f',
5434 100 * (1 - $term_total / $plan->base_total) );
5435 my $permonth = sprintf('%.2f', $term_total / $months);
5436 my $detail = $self->mt('discount on item'). ' '.
5437 join(', ', map { "#$_" } $plan->pkgnums)
5440 # discounts for non-integer months don't work anyway
5441 $months = sprintf("%d", $months);
5444 description => $self->mt('Save [_1]% by paying for [_2] months',
5446 amount => $self->mt('[_1] ([_2] per month)',
5447 $term_total, $money_char.$permonth),
5448 ext_description => ($detail || ''),
5451 sort { $b <=> $a } keys %plans;
5455 =item call_details [ OPTION => VALUE ... ]
5457 Returns an array of CSV strings representing the call details for this invoice
5458 The only option available is the boolean prepend_billed_number
5463 my ($self, %opt) = @_;
5465 my $format_function = sub { shift };
5467 if ($opt{prepend_billed_number}) {
5468 $format_function = sub {
5472 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5477 my @details = map { $_->details( 'format_function' => $format_function,
5478 'escape_function' => sub{ return() },
5482 $self->cust_bill_pkg;
5483 my $header = $details[0];
5484 ( $header, grep { $_ ne $header } @details );
5494 =item process_reprint
5498 sub process_reprint {
5499 process_re_X('print', @_);
5502 =item process_reemail
5506 sub process_reemail {
5507 process_re_X('email', @_);
5515 process_re_X('fax', @_);
5523 process_re_X('ftp', @_);
5530 sub process_respool {
5531 process_re_X('spool', @_);
5534 use Storable qw(thaw);
5538 my( $method, $job ) = ( shift, shift );
5539 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5541 my $param = thaw(decode_base64(shift));
5542 warn Dumper($param) if $DEBUG;
5553 my($method, $job, %param ) = @_;
5555 warn "re_X $method for job $job with param:\n".
5556 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5559 #some false laziness w/search/cust_bill.html
5561 my $orderby = 'ORDER BY cust_bill._date';
5563 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5565 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5567 my @cust_bill = qsearch( {
5568 #'select' => "cust_bill.*",
5569 'table' => 'cust_bill',
5570 'addl_from' => $addl_from,
5572 'extra_sql' => $extra_sql,
5573 'order_by' => $orderby,
5577 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5579 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5582 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5583 foreach my $cust_bill ( @cust_bill ) {
5584 $cust_bill->$method();
5586 if ( $job ) { #progressbar foo
5588 if ( time - $min_sec > $last ) {
5589 my $error = $job->update_statustext(
5590 int( 100 * $num / scalar(@cust_bill) )
5592 die $error if $error;
5603 =head1 CLASS METHODS
5609 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5614 my ($class, $start, $end) = @_;
5616 $class->paid_sql($start, $end). ' - '.
5617 $class->credited_sql($start, $end);
5622 Returns an SQL fragment to retreive the net amount (charged minus credited).
5627 my ($class, $start, $end) = @_;
5628 'charged - '. $class->credited_sql($start, $end);
5633 Returns an SQL fragment to retreive the amount paid against this invoice.
5638 my ($class, $start, $end) = @_;
5639 $start &&= "AND cust_bill_pay._date <= $start";
5640 $end &&= "AND cust_bill_pay._date > $end";
5641 $start = '' unless defined($start);
5642 $end = '' unless defined($end);
5643 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5644 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5649 Returns an SQL fragment to retreive the amount credited against this invoice.
5654 my ($class, $start, $end) = @_;
5655 $start &&= "AND cust_credit_bill._date <= $start";
5656 $end &&= "AND cust_credit_bill._date > $end";
5657 $start = '' unless defined($start);
5658 $end = '' unless defined($end);
5659 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5660 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5665 Returns an SQL fragment to retrieve the due date of an invoice.
5666 Currently only supported on PostgreSQL.
5671 my $conf = new FS::Conf;
5675 cust_bill.invoice_terms,
5676 cust_main.invoice_terms,
5677 \''.($conf->config('invoice_default_terms') || '').'\'
5678 ), E\'Net (\\\\d+)\'
5680 ) * 86400 + cust_bill._date'
5683 =item search_sql_where HASHREF
5685 Class method which returns an SQL WHERE fragment to search for parameters
5686 specified in HASHREF. Valid parameters are
5692 List reference of start date, end date, as UNIX timestamps.
5702 List reference of charged limits (exclusive).
5706 List reference of charged limits (exclusive).
5710 flag, return open invoices only
5714 flag, return net invoices only
5718 =item newest_percust
5722 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5726 sub search_sql_where {
5727 my($class, $param) = @_;
5729 warn "$me search_sql_where called with params: \n".
5730 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5736 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5737 push @search, "cust_main.agentnum = $1";
5741 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5742 push @search, "cust_main.refnum = $1";
5746 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5747 push @search, "cust_bill.custnum = $1";
5751 if ( $param->{'cust_classnum'} ) {
5752 my $classnums = $param->{'cust_classnum'};
5753 $classnums = [ $classnums ] if !ref($classnums);
5754 $classnums = [ grep /^\d+$/, @$classnums ];
5755 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5760 if ( $param->{_date} ) {
5761 my($beginning, $ending) = @{$param->{_date}};
5763 push @search, "cust_bill._date >= $beginning",
5764 "cust_bill._date < $ending";
5768 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5769 push @search, "cust_bill.invnum >= $1";
5771 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5772 push @search, "cust_bill.invnum <= $1";
5776 if ( $param->{charged} ) {
5777 my @charged = ref($param->{charged})
5778 ? @{ $param->{charged} }
5779 : ($param->{charged});
5781 push @search, map { s/^charged/cust_bill.charged/; $_; }
5785 my $owed_sql = FS::cust_bill->owed_sql;
5788 if ( $param->{owed} ) {
5789 my @owed = ref($param->{owed})
5790 ? @{ $param->{owed} }
5792 push @search, map { s/^owed/$owed_sql/; $_; }
5797 push @search, "0 != $owed_sql"
5798 if $param->{'open'};
5799 push @search, '0 != '. FS::cust_bill->net_sql
5803 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5804 if $param->{'days'};
5807 if ( $param->{'newest_percust'} ) {
5809 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5810 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5812 my @newest_where = map { my $x = $_;
5813 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5816 grep ! /^cust_main./, @search;
5817 my $newest_where = scalar(@newest_where)
5818 ? ' AND '. join(' AND ', @newest_where)
5822 push @search, "cust_bill._date = (
5823 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5824 WHERE newest_cust_bill.custnum = cust_bill.custnum
5830 #promised_date - also has an option to accept nulls
5831 if ( $param->{promised_date} ) {
5832 my($beginning, $ending, $null) = @{$param->{promised_date}};
5834 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5835 "cust_bill.promised_date < $ending )" .
5836 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5839 #agent virtualization
5840 my $curuser = $FS::CurrentUser::CurrentUser;
5841 if ( $curuser->username eq 'fs_queue'
5842 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5844 my $newuser = qsearchs('access_user', {
5845 'username' => $username,
5849 $curuser = $newuser;
5851 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5854 push @search, $curuser->agentnums_sql;
5856 join(' AND ', @search );
5868 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5869 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base