4 use vars qw( @ISA $DEBUG $me
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') || '$';
62 FS::cust_bill - Object methods for cust_bill records
68 $record = new FS::cust_bill \%hash;
69 $record = new FS::cust_bill { 'column' => 'value' };
71 $error = $record->insert;
73 $error = $new_record->replace($old_record);
75 $error = $record->delete;
77 $error = $record->check;
79 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
81 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
83 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
85 @cust_pay_objects = $cust_bill->cust_pay;
87 $tax_amount = $record->tax;
89 @lines = $cust_bill->print_text;
90 @lines = $cust_bill->print_text $time;
94 An FS::cust_bill object represents an invoice; a declaration that a customer
95 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
96 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
97 following fields are currently supported:
103 =item invnum - primary key (assigned automatically for new invoices)
105 =item custnum - customer (see L<FS::cust_main>)
107 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
108 L<Time::Local> and L<Date::Parse> for conversion functions.
110 =item charged - amount of this invoice
112 =item invoice_terms - optional terms override for this specific invoice
116 Customer info at invoice generation time
120 =item previous_balance
122 =item billing_balance
130 =item printed - deprecated
138 =item closed - books closed flag, empty or `Y'
140 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
142 =item agent_invid - legacy invoice number
144 =item promised_date - customer promised payment date, for collection
154 Creates a new invoice. To add the invoice to the database, see L<"insert">.
155 Invoices are normally created by calling the bill method of a customer object
156 (see L<FS::cust_main>).
160 sub table { 'cust_bill'; }
162 sub cust_linked { $_[0]->cust_main_custnum; }
163 sub cust_unlinked_msg {
165 "WARNING: can't find cust_main.custnum ". $self->custnum.
166 ' (cust_bill.invnum '. $self->invnum. ')';
171 Adds this invoice to the database ("Posts" the invoice). If there is an error,
172 returns the error, otherwise returns false.
178 warn "$me insert called\n" if $DEBUG;
180 local $SIG{HUP} = 'IGNORE';
181 local $SIG{INT} = 'IGNORE';
182 local $SIG{QUIT} = 'IGNORE';
183 local $SIG{TERM} = 'IGNORE';
184 local $SIG{TSTP} = 'IGNORE';
185 local $SIG{PIPE} = 'IGNORE';
187 my $oldAutoCommit = $FS::UID::AutoCommit;
188 local $FS::UID::AutoCommit = 0;
191 my $error = $self->SUPER::insert;
193 $dbh->rollback if $oldAutoCommit;
197 if ( $self->get('cust_bill_pkg') ) {
198 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
199 $cust_bill_pkg->invnum($self->invnum);
200 my $error = $cust_bill_pkg->insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "can't create invoice line item: $error";
208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
215 This method now works but you probably shouldn't use it. Instead, apply a
216 credit against the invoice.
218 Using this method to delete invoices outright is really, really bad. There
219 would be no record you ever posted this invoice, and there are no check to
220 make sure charged = 0 or that there are no associated cust_bill_pkg records.
222 Really, don't use it.
228 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
230 local $SIG{HUP} = 'IGNORE';
231 local $SIG{INT} = 'IGNORE';
232 local $SIG{QUIT} = 'IGNORE';
233 local $SIG{TERM} = 'IGNORE';
234 local $SIG{TSTP} = 'IGNORE';
235 local $SIG{PIPE} = 'IGNORE';
237 my $oldAutoCommit = $FS::UID::AutoCommit;
238 local $FS::UID::AutoCommit = 0;
241 foreach my $table (qw(
253 foreach my $linked ( $self->$table() ) {
254 my $error = $linked->delete;
256 $dbh->rollback if $oldAutoCommit;
263 my $error = $self->SUPER::delete(@_);
265 $dbh->rollback if $oldAutoCommit;
269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275 =item replace [ OLD_RECORD ]
277 You can, but probably shouldn't modify invoices...
279 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
280 supplied, replaces this record. If there is an error, returns the error,
281 otherwise returns false.
285 #replace can be inherited from Record.pm
287 # replace_check is now the preferred way to #implement replace data checks
288 # (so $object->replace() works without an argument)
291 my( $new, $old ) = ( shift, shift );
292 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
293 #return "Can't change _date!" unless $old->_date eq $new->_date;
294 return "Can't change _date" unless $old->_date == $new->_date;
295 return "Can't change charged" unless $old->charged == $new->charged
296 || $old->charged == 0
297 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
303 =item add_cc_surcharge
309 sub add_cc_surcharge {
310 my ($self, $pkgnum, $amount) = (shift, shift, shift);
313 my $cust_bill_pkg = new FS::cust_bill_pkg({
314 'invnum' => $self->invnum,
318 $error = $cust_bill_pkg->insert;
319 return $error if $error;
321 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
322 $self->charged($self->charged+$amount);
323 $error = $self->replace;
324 return $error if $error;
326 $self->apply_payments_and_credits;
332 Checks all fields to make sure this is a valid invoice. If there is an error,
333 returns the error, otherwise returns false. Called by the insert and replace
342 $self->ut_numbern('invnum')
343 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
344 || $self->ut_numbern('_date')
345 || $self->ut_money('charged')
346 || $self->ut_numbern('printed')
347 || $self->ut_enum('closed', [ '', 'Y' ])
348 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
349 || $self->ut_numbern('agent_invid') #varchar?
351 return $error if $error;
353 $self->_date(time) unless $self->_date;
355 $self->printed(0) if $self->printed eq '';
362 Returns the displayed invoice number for this invoice: agent_invid if
363 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
369 my $conf = $self->conf;
370 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
371 return $self->agent_invid;
373 return $self->invnum;
379 Returns the customer's last invoice before this one.
385 if ( !$self->get('previous_bill') ) {
386 $self->set('previous_bill', qsearchs({
387 'table' => 'cust_bill',
388 'hashref' => { 'custnum' => $self->custnum,
389 '_date' => { op=>'<', value=>$self->_date } },
390 'order_by' => 'ORDER BY _date DESC LIMIT 1',
393 $self->get('previous_bill');
398 Returns a list consisting of the total previous balance for this customer,
399 followed by the previous outstanding invoices (as FS::cust_bill objects also).
406 my @cust_bill = sort { $a->_date <=> $b->_date }
407 grep { $_->owed != 0 }
408 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
409 #'_date' => { op=>'<', value=>$self->_date },
410 'invnum' => { op=>'<', value=>$self->invnum },
413 foreach ( @cust_bill ) { $total += $_->owed; }
417 =item enable_previous
419 Whether to show the 'Previous Charges' section when printing this invoice.
420 The negation of the 'disable_previous_balance' config setting.
424 sub enable_previous {
426 my $agentnum = $self->cust_main->agentnum;
427 !$self->conf->exists('disable_previous_balance', $agentnum);
432 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
439 { 'table' => 'cust_bill_pkg',
440 'hashref' => { 'invnum' => $self->invnum },
441 'order_by' => 'ORDER BY billpkgnum',
446 =item cust_bill_pkg_pkgnum PKGNUM
448 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
453 sub cust_bill_pkg_pkgnum {
454 my( $self, $pkgnum ) = @_;
456 { 'table' => 'cust_bill_pkg',
457 'hashref' => { 'invnum' => $self->invnum,
460 'order_by' => 'ORDER BY billpkgnum',
467 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
474 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
475 $self->cust_bill_pkg;
477 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
482 Returns true if any of the packages (or their definitions) corresponding to the
483 line items for this invoice have the no_auto flag set.
489 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
492 =item open_cust_bill_pkg
494 Returns the open line items for this invoice.
496 Note that cust_bill_pkg with both setup and recur fees are returned as two
497 separate line items, each with only one fee.
501 # modeled after cust_main::open_cust_bill
502 sub open_cust_bill_pkg {
505 # grep { $_->owed > 0 } $self->cust_bill_pkg
507 my %other = ( 'recur' => 'setup',
508 'setup' => 'recur', );
510 foreach my $field ( qw( recur setup )) {
511 push @open, map { $_->set( $other{$field}, 0 ); $_; }
512 grep { $_->owed($field) > 0 }
513 $self->cust_bill_pkg;
519 =item cust_bill_event
521 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
525 sub cust_bill_event {
527 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
530 =item num_cust_bill_event
532 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
536 sub num_cust_bill_event {
539 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
540 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
541 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
542 $sth->fetchrow_arrayref->[0];
547 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
551 #false laziness w/cust_pkg.pm
555 'table' => 'cust_event',
556 'addl_from' => 'JOIN part_event USING ( eventpart )',
557 'hashref' => { 'tablenum' => $self->invnum },
558 'extra_sql' => " AND eventtable = 'cust_bill' ",
564 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
568 #false laziness w/cust_pkg.pm
572 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
573 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
574 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
575 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
576 $sth->fetchrow_arrayref->[0];
581 Returns the customer (see L<FS::cust_main>) for this invoice.
587 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
590 =item cust_suspend_if_balance_over AMOUNT
592 Suspends the customer associated with this invoice if the total amount owed on
593 this invoice and all older invoices is greater than the specified amount.
595 Returns a list: an empty list on success or a list of errors.
599 sub cust_suspend_if_balance_over {
600 my( $self, $amount ) = ( shift, shift );
601 my $cust_main = $self->cust_main;
602 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
605 $cust_main->suspend(@_);
611 Depreciated. See the cust_credited method.
613 #Returns a list consisting of the total previous credited (see
614 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
615 #outstanding credits (FS::cust_credit objects).
621 croak "FS::cust_bill->cust_credit depreciated; see ".
622 "FS::cust_bill->cust_credit_bill";
625 #my @cust_credit = sort { $a->_date <=> $b->_date }
626 # grep { $_->credited != 0 && $_->_date < $self->_date }
627 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
629 #foreach (@cust_credit) { $total += $_->credited; }
630 #$total, @cust_credit;
635 Depreciated. See the cust_bill_pay method.
637 #Returns all payments (see L<FS::cust_pay>) for this invoice.
643 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
645 #sort { $a->_date <=> $b->_date }
646 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
652 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
655 sub cust_bill_pay_batch {
657 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
662 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
668 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
669 sort { $a->_date <=> $b->_date }
670 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
675 =item cust_credit_bill
677 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
683 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
684 sort { $a->_date <=> $b->_date }
685 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
689 sub cust_credit_bill {
690 shift->cust_credited(@_);
693 #=item cust_bill_pay_pkgnum PKGNUM
695 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
696 #with matching pkgnum.
700 #sub cust_bill_pay_pkgnum {
701 # my( $self, $pkgnum ) = @_;
702 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
703 # sort { $a->_date <=> $b->_date }
704 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
705 # 'pkgnum' => $pkgnum,
710 =item cust_bill_pay_pkg PKGNUM
712 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
713 applied against the matching pkgnum.
717 sub cust_bill_pay_pkg {
718 my( $self, $pkgnum ) = @_;
721 'select' => 'cust_bill_pay_pkg.*',
722 'table' => 'cust_bill_pay_pkg',
723 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
724 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
725 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
726 " AND cust_bill_pkg.pkgnum = $pkgnum",
731 #=item cust_credited_pkgnum PKGNUM
733 #=item cust_credit_bill_pkgnum PKGNUM
735 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
736 #with matching pkgnum.
740 #sub cust_credited_pkgnum {
741 # my( $self, $pkgnum ) = @_;
742 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
743 # sort { $a->_date <=> $b->_date }
744 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
745 # 'pkgnum' => $pkgnum,
750 #sub cust_credit_bill_pkgnum {
751 # shift->cust_credited_pkgnum(@_);
754 =item cust_credit_bill_pkg PKGNUM
756 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
757 applied against the matching pkgnum.
761 sub cust_credit_bill_pkg {
762 my( $self, $pkgnum ) = @_;
765 'select' => 'cust_credit_bill_pkg.*',
766 'table' => 'cust_credit_bill_pkg',
767 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
768 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
769 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
770 " AND cust_bill_pkg.pkgnum = $pkgnum",
775 =item cust_bill_batch
777 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
781 sub cust_bill_batch {
783 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
788 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
789 hash keyed by term length.
795 FS::discount_plan->all($self);
800 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
807 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
809 foreach (@taxlines) { $total += $_->setup; }
815 Returns the amount owed (still outstanding) on this invoice, which is charged
816 minus all payment applications (see L<FS::cust_bill_pay>) and credit
817 applications (see L<FS::cust_credit_bill>).
823 my $balance = $self->charged;
824 $balance -= $_->amount foreach ( $self->cust_bill_pay );
825 $balance -= $_->amount foreach ( $self->cust_credited );
826 $balance = sprintf( "%.2f", $balance);
827 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
832 my( $self, $pkgnum ) = @_;
834 #my $balance = $self->charged;
836 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
838 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
839 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
841 $balance = sprintf( "%.2f", $balance);
842 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
848 Returns true if this invoice should be hidden. See the
849 selfservice-hide_invoices-taxclass configuraiton setting.
855 my $conf = $self->conf;
856 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
858 my @cust_bill_pkg = $self->cust_bill_pkg;
859 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
860 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
863 =item apply_payments_and_credits [ OPTION => VALUE ... ]
865 Applies unapplied payments and credits to this invoice.
867 A hash of optional arguments may be passed. Currently "manual" is supported.
868 If true, a payment receipt is sent instead of a statement when
869 'payment_receipt_email' configuration option is set.
871 If there is an error, returns the error, otherwise returns false.
875 sub apply_payments_and_credits {
876 my( $self, %options ) = @_;
877 my $conf = $self->conf;
879 local $SIG{HUP} = 'IGNORE';
880 local $SIG{INT} = 'IGNORE';
881 local $SIG{QUIT} = 'IGNORE';
882 local $SIG{TERM} = 'IGNORE';
883 local $SIG{TSTP} = 'IGNORE';
884 local $SIG{PIPE} = 'IGNORE';
886 my $oldAutoCommit = $FS::UID::AutoCommit;
887 local $FS::UID::AutoCommit = 0;
890 $self->select_for_update; #mutex
892 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
893 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
895 if ( $conf->exists('pkg-balances') ) {
896 # limit @payments & @credits to those w/ a pkgnum grepped from $self
897 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
898 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
899 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
902 while ( $self->owed > 0 and ( @payments || @credits ) ) {
905 if ( @payments && @credits ) {
907 #decide which goes first by weight of top (unapplied) line item
909 my @open_lineitems = $self->open_cust_bill_pkg;
912 max( map { $_->part_pkg->pay_weight || 0 }
917 my $max_credit_weight =
918 max( map { $_->part_pkg->credit_weight || 0 }
924 #if both are the same... payments first? it has to be something
925 if ( $max_pay_weight >= $max_credit_weight ) {
931 } elsif ( @payments ) {
933 } elsif ( @credits ) {
936 die "guru meditation #12 and 35";
940 if ( $app eq 'pay' ) {
942 my $payment = shift @payments;
943 $unapp_amount = $payment->unapplied;
944 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
945 $app->pkgnum( $payment->pkgnum )
946 if $conf->exists('pkg-balances') && $payment->pkgnum;
948 } elsif ( $app eq 'credit' ) {
950 my $credit = shift @credits;
951 $unapp_amount = $credit->credited;
952 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
953 $app->pkgnum( $credit->pkgnum )
954 if $conf->exists('pkg-balances') && $credit->pkgnum;
957 die "guru meditation #12 and 35";
961 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
962 warn "owed_pkgnum ". $app->pkgnum;
963 $owed = $self->owed_pkgnum($app->pkgnum);
967 next unless $owed > 0;
969 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
970 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
972 $app->invnum( $self->invnum );
974 my $error = $app->insert(%options);
976 $dbh->rollback if $oldAutoCommit;
977 return "Error inserting ". $app->table. " record: $error";
979 die $error if $error;
983 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
988 =item generate_email OPTION => VALUE ...
996 sender address, required
1000 alternate template name, optional
1004 text attachment arrayref, optional
1008 email subject, optional
1012 notice name instead of "Invoice", optional
1016 Returns an argument list to be passed to L<FS::Misc::send_email>.
1022 sub generate_email {
1026 my $conf = $self->conf;
1028 my $me = '[FS::cust_bill::generate_email]';
1031 'from' => $args{'from'},
1032 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1036 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1037 'template' => $args{'template'},
1038 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1039 'no_coupon' => $args{'no_coupon'},
1042 my $cust_main = $self->cust_main;
1044 if (ref($args{'to'}) eq 'ARRAY') {
1045 $return{'to'} = $args{'to'};
1047 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1048 $cust_main->invoicing_list
1052 if ( $conf->exists('invoice_html') ) {
1054 warn "$me creating HTML/text multipart message"
1057 $return{'nobody'} = 1;
1059 my $alternative = build MIME::Entity
1060 'Type' => 'multipart/alternative',
1061 #'Encoding' => '7bit',
1062 'Disposition' => 'inline'
1066 if ( $conf->exists('invoice_email_pdf')
1067 and scalar($conf->config('invoice_email_pdf_note')) ) {
1069 warn "$me using 'invoice_email_pdf_note' in multipart message"
1071 $data = [ map { $_ . "\n" }
1072 $conf->config('invoice_email_pdf_note')
1077 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1079 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1080 $data = $args{'print_text'};
1082 $data = [ $self->print_text(\%opt) ];
1087 $alternative->attach(
1088 'Type' => 'text/plain',
1089 'Encoding' => 'quoted-printable',
1090 #'Encoding' => '7bit',
1092 'Disposition' => 'inline',
1099 if ( $conf->exists('invoice_email_pdf')
1100 and scalar($conf->config('invoice_email_pdf_note')) ) {
1102 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1106 $args{'from'} =~ /\@([\w\.\-]+)/;
1107 my $from = $1 || 'example.com';
1108 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1111 my $agentnum = $cust_main->agentnum;
1112 if ( defined($args{'template'}) && length($args{'template'})
1113 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1116 $logo = 'logo_'. $args{'template'}. '.png';
1120 my $image_data = $conf->config_binary( $logo, $agentnum);
1122 $image = build MIME::Entity
1123 'Type' => 'image/png',
1124 'Encoding' => 'base64',
1125 'Data' => $image_data,
1126 'Filename' => 'logo.png',
1127 'Content-ID' => "<$content_id>",
1130 if ($conf->exists('invoice-barcode')) {
1131 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1132 $barcode = build MIME::Entity
1133 'Type' => 'image/png',
1134 'Encoding' => 'base64',
1135 'Data' => $self->invoice_barcode(0),
1136 'Filename' => 'barcode.png',
1137 'Content-ID' => "<$barcode_content_id>",
1139 $opt{'barcode_cid'} = $barcode_content_id;
1142 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1145 $alternative->attach(
1146 'Type' => 'text/html',
1147 'Encoding' => 'quoted-printable',
1148 'Data' => [ '<html>',
1151 ' '. encode_entities($return{'subject'}),
1154 ' <body bgcolor="#e8e8e8">',
1159 'Disposition' => 'inline',
1160 #'Filename' => 'invoice.pdf',
1164 my @otherparts = ();
1165 if ( $cust_main->email_csv_cdr ) {
1167 push @otherparts, build MIME::Entity
1168 'Type' => 'text/csv',
1169 'Encoding' => '7bit',
1170 'Data' => [ map { "$_\n" }
1171 $self->call_details('prepend_billed_number' => 1)
1173 'Disposition' => 'attachment',
1174 'Filename' => 'usage-'. $self->invnum. '.csv',
1179 if ( $conf->exists('invoice_email_pdf') ) {
1184 # multipart/alternative
1190 my $related = build MIME::Entity 'Type' => 'multipart/related',
1191 'Encoding' => '7bit';
1193 #false laziness w/Misc::send_email
1194 $related->head->replace('Content-type',
1195 $related->mime_type.
1196 '; boundary="'. $related->head->multipart_boundary. '"'.
1197 '; type=multipart/alternative'
1200 $related->add_part($alternative);
1202 $related->add_part($image) if $image;
1204 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1206 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1210 #no other attachment:
1212 # multipart/alternative
1217 $return{'content-type'} = 'multipart/related';
1218 if ($conf->exists('invoice-barcode') && $barcode) {
1219 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1221 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1223 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1224 #$return{'disposition'} = 'inline';
1230 if ( $conf->exists('invoice_email_pdf') ) {
1231 warn "$me creating PDF attachment"
1234 #mime parts arguments a la MIME::Entity->build().
1235 $return{'mimeparts'} = [
1236 { $self->mimebuild_pdf(\%opt) }
1240 if ( $conf->exists('invoice_email_pdf')
1241 and scalar($conf->config('invoice_email_pdf_note')) ) {
1243 warn "$me using 'invoice_email_pdf_note'"
1245 $return{'body'} = [ map { $_ . "\n" }
1246 $conf->config('invoice_email_pdf_note')
1251 warn "$me not using 'invoice_email_pdf_note'"
1253 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1254 $return{'body'} = $args{'print_text'};
1256 $return{'body'} = [ $self->print_text(\%opt) ];
1269 Returns a list suitable for passing to MIME::Entity->build(), representing
1270 this invoice as PDF attachment.
1277 'Type' => 'application/pdf',
1278 'Encoding' => 'base64',
1279 'Data' => [ $self->print_pdf(@_) ],
1280 'Disposition' => 'attachment',
1281 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1285 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1287 Sends this invoice to the destinations configured for this customer: sends
1288 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1290 Options can be passed as a hashref (recommended) or as a list of up to
1291 four values for templatename, agentnum, invoice_from and amount.
1293 I<template>, if specified, is the name of a suffix for alternate invoices.
1295 I<agentnum>, if specified, means that this invoice will only be sent for customers
1296 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1297 single agent) or an arrayref of agentnums.
1299 I<invoice_from>, if specified, overrides the default email invoice From: address.
1301 I<amount>, if specified, only sends the invoice if the total amount owed on this
1302 invoice and all older invoices is greater than the specified amount.
1304 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1308 sub queueable_send {
1311 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1312 or die "invalid invoice number: " . $opt{invnum};
1314 my @args = ( $opt{template}, $opt{agentnum} );
1315 push @args, $opt{invoice_from}
1316 if exists($opt{invoice_from}) && $opt{invoice_from};
1318 my $error = $self->send( @args );
1319 die $error if $error;
1325 my $conf = $self->conf;
1327 my( $template, $invoice_from, $notice_name );
1329 my $balance_over = 0;
1333 $template = $opt->{'template'} || '';
1334 if ( $agentnums = $opt->{'agentnum'} ) {
1335 $agentnums = [ $agentnums ] unless ref($agentnums);
1337 $invoice_from = $opt->{'invoice_from'};
1338 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1339 $notice_name = $opt->{'notice_name'};
1341 $template = scalar(@_) ? shift : '';
1342 if ( scalar(@_) && $_[0] ) {
1343 $agentnums = ref($_[0]) ? shift : [ shift ];
1345 $invoice_from = shift if scalar(@_);
1346 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1349 my $cust_main = $self->cust_main;
1351 return 'N/A' unless ! $agentnums
1352 or grep { $_ == $cust_main->agentnum } @$agentnums;
1355 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1357 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1358 $conf->config('invoice_from', $cust_main->agentnum );
1361 'template' => $template,
1362 'invoice_from' => $invoice_from,
1363 'notice_name' => ( $notice_name || 'Invoice' ),
1366 my @invoicing_list = $cust_main->invoicing_list;
1368 #$self->email_invoice(\%opt)
1370 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1371 && ! $self->invoice_noemail;
1373 #$self->print_invoice(\%opt)
1375 if grep { $_ eq 'POST' } @invoicing_list; #postal
1377 $self->fax_invoice(\%opt)
1378 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1384 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1386 Emails this invoice.
1388 Options can be passed as a hashref (recommended) or as a list of up to
1389 two values for templatename and invoice_from.
1391 I<template>, if specified, is the name of a suffix for alternate invoices.
1393 I<invoice_from>, if specified, overrides the default email invoice From: address.
1395 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1399 sub queueable_email {
1402 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1403 or die "invalid invoice number: " . $opt{invnum};
1405 my %args = ( 'template' => $opt{template} );
1406 $args{$_} = $opt{$_}
1407 foreach grep { exists($opt{$_}) && $opt{$_} }
1408 qw( invoice_from notice_name no_coupon );
1410 my $error = $self->email( \%args );
1411 die $error if $error;
1415 #sub email_invoice {
1418 return if $self->hide;
1419 my $conf = $self->conf;
1421 my( $template, $invoice_from, $notice_name, $no_coupon );
1424 $template = $opt->{'template'} || '';
1425 $invoice_from = $opt->{'invoice_from'};
1426 $notice_name = $opt->{'notice_name'} || 'Invoice';
1427 $no_coupon = $opt->{'no_coupon'} || 0;
1429 $template = scalar(@_) ? shift : '';
1430 $invoice_from = shift if scalar(@_);
1431 $notice_name = 'Invoice';
1435 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1436 $conf->config('invoice_from', $self->cust_main->agentnum );
1438 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1439 $self->cust_main->invoicing_list;
1441 if ( ! @invoicing_list ) { #no recipients
1442 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1443 die 'No recipients for customer #'. $self->custnum;
1445 #default: better to notify this person than silence
1446 @invoicing_list = ($invoice_from);
1450 my $subject = $self->email_subject($template);
1452 my $error = send_email(
1453 $self->generate_email(
1454 'from' => $invoice_from,
1455 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1456 'subject' => $subject,
1457 'template' => $template,
1458 'notice_name' => $notice_name,
1459 'no_coupon' => $no_coupon,
1462 die "can't email invoice: $error\n" if $error;
1463 #die "$error\n" if $error;
1469 my $conf = $self->conf;
1471 #my $template = scalar(@_) ? shift : '';
1474 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1477 my $cust_main = $self->cust_main;
1478 my $name = $cust_main->name;
1479 my $name_short = $cust_main->name_short;
1480 my $invoice_number = $self->invnum;
1481 my $invoice_date = $self->_date_pretty;
1483 eval qq("$subject");
1486 =item lpr_data HASHREF | [ TEMPLATE ]
1488 Returns the postscript or plaintext for this invoice as an arrayref.
1490 Options can be passed as a hashref (recommended) or as a single optional value
1493 I<template>, if specified, is the name of a suffix for alternate invoices.
1495 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1501 my $conf = $self->conf;
1502 my( $template, $notice_name );
1505 $template = $opt->{'template'} || '';
1506 $notice_name = $opt->{'notice_name'} || 'Invoice';
1508 $template = scalar(@_) ? shift : '';
1509 $notice_name = 'Invoice';
1513 'template' => $template,
1514 'notice_name' => $notice_name,
1517 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1518 [ $self->$method( \%opt ) ];
1521 =item print HASHREF | [ TEMPLATE ]
1523 Prints this invoice.
1525 Options can be passed as a hashref (recommended) or as a single optional
1528 I<template>, if specified, is the name of a suffix for alternate invoices.
1530 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1534 #sub print_invoice {
1537 return if $self->hide;
1538 my $conf = $self->conf;
1540 my( $template, $notice_name );
1543 $template = $opt->{'template'} || '';
1544 $notice_name = $opt->{'notice_name'} || 'Invoice';
1546 $template = scalar(@_) ? shift : '';
1547 $notice_name = 'Invoice';
1551 'template' => $template,
1552 'notice_name' => $notice_name,
1555 if($conf->exists('invoice_print_pdf')) {
1556 # Add the invoice to the current batch.
1557 $self->batch_invoice(\%opt);
1561 $self->lpr_data(\%opt),
1562 'agentnum' => $self->cust_main->agentnum,
1567 =item fax_invoice HASHREF | [ TEMPLATE ]
1571 Options can be passed as a hashref (recommended) or as a single optional
1574 I<template>, if specified, is the name of a suffix for alternate invoices.
1576 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1582 return if $self->hide;
1583 my $conf = $self->conf;
1585 my( $template, $notice_name );
1588 $template = $opt->{'template'} || '';
1589 $notice_name = $opt->{'notice_name'} || 'Invoice';
1591 $template = scalar(@_) ? shift : '';
1592 $notice_name = 'Invoice';
1595 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1596 unless $conf->exists('invoice_latex');
1598 my $dialstring = $self->cust_main->getfield('fax');
1602 'template' => $template,
1603 'notice_name' => $notice_name,
1606 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1607 'dialstring' => $dialstring,
1609 die $error if $error;
1613 =item batch_invoice [ HASHREF ]
1615 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1616 isn't an open batch, one will be created.
1621 my ($self, $opt) = @_;
1622 my $bill_batch = $self->get_open_bill_batch;
1623 my $cust_bill_batch = FS::cust_bill_batch->new({
1624 batchnum => $bill_batch->batchnum,
1625 invnum => $self->invnum,
1627 return $cust_bill_batch->insert($opt);
1630 =item get_open_batch
1632 Returns the currently open batch as an FS::bill_batch object, creating a new
1633 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1638 sub get_open_bill_batch {
1640 my $conf = $self->conf;
1641 my $hashref = { status => 'O' };
1642 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1643 ? $self->cust_main->agentnum
1645 my $batch = qsearchs('bill_batch', $hashref);
1646 return $batch if $batch;
1647 $batch = FS::bill_batch->new($hashref);
1648 my $error = $batch->insert;
1649 die $error if $error;
1653 =item ftp_invoice [ TEMPLATENAME ]
1655 Sends this invoice data via FTP.
1657 TEMPLATENAME is unused?
1663 my $conf = $self->conf;
1664 my $template = scalar(@_) ? shift : '';
1667 'protocol' => 'ftp',
1668 'server' => $conf->config('cust_bill-ftpserver'),
1669 'username' => $conf->config('cust_bill-ftpusername'),
1670 'password' => $conf->config('cust_bill-ftppassword'),
1671 'dir' => $conf->config('cust_bill-ftpdir'),
1672 'format' => $conf->config('cust_bill-ftpformat'),
1676 =item spool_invoice [ TEMPLATENAME ]
1678 Spools this invoice data (see L<FS::spool_csv>)
1680 TEMPLATENAME is unused?
1686 my $conf = $self->conf;
1687 my $template = scalar(@_) ? shift : '';
1690 'format' => $conf->config('cust_bill-spoolformat'),
1691 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1695 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1697 Like B<send>, but only sends the invoice if it is the newest open invoice for
1702 sub send_if_newest {
1707 grep { $_->owed > 0 }
1708 qsearch('cust_bill', {
1709 'custnum' => $self->custnum,
1710 #'_date' => { op=>'>', value=>$self->_date },
1711 'invnum' => { op=>'>', value=>$self->invnum },
1718 =item send_csv OPTION => VALUE, ...
1720 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1724 protocol - currently only "ftp"
1730 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1731 and YYMMDDHHMMSS is a timestamp.
1733 See L</print_csv> for a description of the output format.
1738 my($self, %opt) = @_;
1742 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1743 mkdir $spooldir, 0700 unless -d $spooldir;
1745 # don't localize dates here, they're a defined format
1746 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1747 my $file = "$spooldir/$tracctnum.csv";
1749 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1751 open(CSV, ">$file") or die "can't open $file: $!";
1759 if ( $opt{protocol} eq 'ftp' ) {
1760 eval "use Net::FTP;";
1762 $net = Net::FTP->new($opt{server}) or die @$;
1764 die "unknown protocol: $opt{protocol}";
1767 $net->login( $opt{username}, $opt{password} )
1768 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1770 $net->binary or die "can't set binary mode";
1772 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1774 $net->put($file) or die "can't put $file: $!";
1784 Spools CSV invoice data.
1790 =item format - 'default' or 'billco'
1792 =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>).
1794 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1796 =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.
1803 my($self, %opt) = @_;
1805 my $cust_main = $self->cust_main;
1807 if ( $opt{'dest'} ) {
1808 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1809 $cust_main->invoicing_list;
1810 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1811 || ! keys %invoicing_list;
1814 if ( $opt{'balanceover'} ) {
1816 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1819 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1820 mkdir $spooldir, 0700 unless -d $spooldir;
1822 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1826 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1827 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1830 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1832 open(CSV, ">>$file") or die "can't open $file: $!";
1833 flock(CSV, LOCK_EX);
1838 if ( lc($opt{'format'}) eq 'billco' ) {
1840 flock(CSV, LOCK_UN);
1845 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1848 open(CSV,">>$file") or die "can't open $file: $!";
1849 flock(CSV, LOCK_EX);
1855 flock(CSV, LOCK_UN);
1862 =item print_csv OPTION => VALUE, ...
1864 Returns CSV data for this invoice.
1868 format - 'default' or 'billco'
1870 Returns a list consisting of two scalars. The first is a single line of CSV
1871 header information for this invoice. The second is one or more lines of CSV
1872 detail information for this invoice.
1874 If I<format> is not specified or "default", the fields of the CSV file are as
1877 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1881 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1883 B<record_type> is C<cust_bill> for the initial header line only. The
1884 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1885 fields are filled in.
1887 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1888 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1891 =item invnum - invoice number
1893 =item custnum - customer number
1895 =item _date - invoice date
1897 =item charged - total invoice amount
1899 =item first - customer first name
1901 =item last - customer first name
1903 =item company - company name
1905 =item address1 - address line 1
1907 =item address2 - address line 1
1917 =item pkg - line item description
1919 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1921 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1923 =item sdate - start date for recurring fee
1925 =item edate - end date for recurring fee
1929 If I<format> is "billco", the fields of the header CSV file are as follows:
1931 +-------------------------------------------------------------------+
1932 | FORMAT HEADER FILE |
1933 |-------------------------------------------------------------------|
1934 | Field | Description | Name | Type | Width |
1935 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1936 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1937 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1938 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1939 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1940 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1941 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1942 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1943 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1944 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1945 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1946 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1947 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1948 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1949 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1950 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1951 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1952 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1953 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1954 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1955 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1956 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1957 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1958 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1959 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1960 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1961 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1962 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1963 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1964 +-------+-------------------------------+------------+------+-------+
1966 If I<format> is "billco", the fields of the detail CSV file are as follows:
1968 FORMAT FOR DETAIL FILE
1970 Field | Description | Name | Type | Width
1971 1 | N/A-Leave Empty | RC | CHAR | 2
1972 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1973 3 | Account Number | TRACCTNUM | CHAR | 15
1974 4 | Invoice Number | TRINVOICE | CHAR | 15
1975 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1976 6 | Transaction Detail | DETAILS | CHAR | 100
1977 7 | Amount | AMT | NUM* | 9
1978 8 | Line Format Control** | LNCTRL | CHAR | 2
1979 9 | Grouping Code | GROUP | CHAR | 2
1980 10 | User Defined | ACCT CODE | CHAR | 15
1985 my($self, %opt) = @_;
1987 eval "use Text::CSV_XS";
1990 my $cust_main = $self->cust_main;
1992 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1994 if ( lc($opt{'format'}) eq 'billco' ) {
1997 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1999 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
2001 my( $previous_balance, @unused ) = $self->previous; #previous balance
2003 my $pmt_cr_applied = 0;
2004 $pmt_cr_applied += $_->{'amount'}
2005 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
2007 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2010 '', # 1 | N/A-Leave Empty CHAR 2
2011 '', # 2 | N/A-Leave Empty CHAR 15
2012 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
2013 $self->invnum, # 4 | Transaction Invoice No CHAR 15
2014 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
2015 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
2016 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2017 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2018 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2019 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2020 '', # 10 | Ancillary Billing Information CHAR 30
2021 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2022 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2025 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2028 $duedate, # 14 | Bill Due Date CHAR 10
2030 $previous_balance, # 15 | Previous Balance NUM* 9
2031 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2032 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2033 $totaldue, # 18 | Total Amt Due NUM* 9
2034 $totaldue, # 19 | Total Amt Due NUM* 9
2035 '', # 20 | 30 Day Aging NUM* 9
2036 '', # 21 | 60 Day Aging NUM* 9
2037 '', # 22 | 90 Day Aging NUM* 9
2038 'N', # 23 | Y/N CHAR 1
2039 '', # 24 | Remittance automation CHAR 100
2040 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2041 $self->custnum, # 26 | Customer Reference Number CHAR 15
2042 '0', # 27 | Federal Tax*** NUM* 9
2043 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2044 '0', # 29 | Other Taxes & Fees*** NUM* 9
2047 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2049 my ($previous_balance) = $self->previous;
2050 $previous_balance = sprintf('%.2f', $previous_balance);
2051 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2057 $self->_items_pkg, #_items_nontax? no sections or anything
2062 $cust_main->agentnum,
2063 $cust_main->agent->agent,
2067 $cust_main->company,
2068 $cust_main->address1,
2069 $cust_main->address2,
2075 time2str("%x", $self->_date),
2080 $self->due_date2str("%x"),
2091 time2str("%x", $self->_date),
2092 sprintf("%.2f", $self->charged),
2093 ( map { $cust_main->getfield($_) }
2094 qw( first last company address1 address2 city state zip country ) ),
2096 ) or die "can't create csv";
2099 my $header = $csv->string. "\n";
2102 if ( lc($opt{'format'}) eq 'billco' ) {
2105 foreach my $item ( $self->_items_pkg ) {
2108 '', # 1 | N/A-Leave Empty CHAR 2
2109 '', # 2 | N/A-Leave Empty CHAR 15
2110 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2111 $self->invnum, # 4 | Invoice Number CHAR 15
2112 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2113 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2114 $item->{'amount'}, # 7 | Amount NUM* 9
2115 '', # 8 | Line Format Control** CHAR 2
2116 '', # 9 | Grouping Code CHAR 2
2117 '', # 10 | User Defined CHAR 15
2120 $detail .= $csv->string. "\n";
2124 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2130 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2132 my($pkg, $setup, $recur, $sdate, $edate);
2133 if ( $cust_bill_pkg->pkgnum ) {
2135 ($pkg, $setup, $recur, $sdate, $edate) = (
2136 $cust_bill_pkg->part_pkg->pkg,
2137 ( $cust_bill_pkg->setup != 0
2138 ? sprintf("%.2f", $cust_bill_pkg->setup )
2140 ( $cust_bill_pkg->recur != 0
2141 ? sprintf("%.2f", $cust_bill_pkg->recur )
2143 ( $cust_bill_pkg->sdate
2144 ? time2str("%x", $cust_bill_pkg->sdate)
2146 ($cust_bill_pkg->edate
2147 ? time2str("%x", $cust_bill_pkg->edate)
2151 } else { #pkgnum tax
2152 next unless $cust_bill_pkg->setup != 0;
2153 $pkg = $cust_bill_pkg->desc;
2154 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2155 ( $sdate, $edate ) = ( '', '' );
2161 ( map { '' } (1..11) ),
2162 ($pkg, $setup, $recur, $sdate, $edate)
2163 ) or die "can't create csv";
2165 $detail .= $csv->string. "\n";
2171 ( $header, $detail );
2177 Pays this invoice with a compliemntary payment. If there is an error,
2178 returns the error, otherwise returns false.
2184 my $cust_pay = new FS::cust_pay ( {
2185 'invnum' => $self->invnum,
2186 'paid' => $self->owed,
2189 'payinfo' => $self->cust_main->payinfo,
2197 Attempts to pay this invoice with a credit card payment via a
2198 Business::OnlinePayment realtime gateway. See
2199 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2200 for supported processors.
2206 $self->realtime_bop( 'CC', @_ );
2211 Attempts to pay this invoice with an electronic check (ACH) payment via a
2212 Business::OnlinePayment realtime gateway. See
2213 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2214 for supported processors.
2220 $self->realtime_bop( 'ECHECK', @_ );
2225 Attempts to pay this invoice with phone bill (LEC) payment via a
2226 Business::OnlinePayment realtime gateway. See
2227 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2228 for supported processors.
2234 $self->realtime_bop( 'LEC', @_ );
2238 my( $self, $method ) = (shift,shift);
2239 my $conf = $self->conf;
2242 my $cust_main = $self->cust_main;
2243 my $balance = $cust_main->balance;
2244 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2245 $amount = sprintf("%.2f", $amount);
2246 return "not run (balance $balance)" unless $amount > 0;
2248 my $description = 'Internet Services';
2249 if ( $conf->exists('business-onlinepayment-description') ) {
2250 my $dtempl = $conf->config('business-onlinepayment-description');
2252 my $agent_obj = $cust_main->agent
2253 or die "can't retreive agent for $cust_main (agentnum ".
2254 $cust_main->agentnum. ")";
2255 my $agent = $agent_obj->agent;
2256 my $pkgs = join(', ',
2257 map { $_->part_pkg->pkg }
2258 grep { $_->pkgnum } $self->cust_bill_pkg
2260 $description = eval qq("$dtempl");
2263 $cust_main->realtime_bop($method, $amount,
2264 'description' => $description,
2265 'invnum' => $self->invnum,
2266 #this didn't do what we want, it just calls apply_payments_and_credits
2268 'apply_to_invoice' => 1,
2271 #this changes application behavior: auto payments
2272 #triggered against a specific invoice are now applied
2273 #to that invoice instead of oldest open.
2279 =item batch_card OPTION => VALUE...
2281 Adds a payment for this invoice to the pending credit card batch (see
2282 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2283 runs the payment using a realtime gateway.
2288 my ($self, %options) = @_;
2289 my $cust_main = $self->cust_main;
2291 $options{invnum} = $self->invnum;
2293 $cust_main->batch_card(%options);
2296 sub _agent_template {
2298 $self->cust_main->agent_template;
2301 sub _agent_invoice_from {
2303 $self->cust_main->agent_invoice_from;
2306 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2308 Returns an text invoice, as a list of lines.
2310 Options can be passed as a hashref (recommended) or as a list of time, template
2311 and then any key/value pairs for any other options.
2313 I<time>, if specified, is used to control the printing of overdue messages. The
2314 default is now. It isn't the date of the invoice; that's the `_date' field.
2315 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2316 L<Time::Local> and L<Date::Parse> for conversion functions.
2318 I<template>, if specified, is the name of a suffix for alternate invoices.
2320 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2326 my( $today, $template, %opt );
2328 %opt = %{ shift() };
2329 $today = delete($opt{'time'}) || '';
2330 $template = delete($opt{template}) || '';
2332 ( $today, $template, %opt ) = @_;
2335 my %params = ( 'format' => 'template' );
2336 $params{'time'} = $today if $today;
2337 $params{'template'} = $template if $template;
2338 $params{$_} = $opt{$_}
2339 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2341 $self->print_generic( %params );
2344 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2346 Internal method - returns a filename of a filled-in LaTeX template for this
2347 invoice (Note: add ".tex" to get the actual filename), and a filename of
2348 an associated logo (with the .eps extension included).
2350 See print_ps and print_pdf for methods that return PostScript and PDF output.
2352 Options can be passed as a hashref (recommended) or as a list of time, template
2353 and then any key/value pairs for any other options.
2355 I<time>, if specified, is used to control the printing of overdue messages. The
2356 default is now. It isn't the date of the invoice; that's the `_date' field.
2357 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2358 L<Time::Local> and L<Date::Parse> for conversion functions.
2360 I<template>, if specified, is the name of a suffix for alternate invoices.
2362 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2368 my $conf = $self->conf;
2369 my( $today, $template, %opt );
2371 %opt = %{ shift() };
2372 $today = delete($opt{'time'}) || '';
2373 $template = delete($opt{template}) || '';
2375 ( $today, $template, %opt ) = @_;
2378 my %params = ( 'format' => 'latex' );
2379 $params{'time'} = $today if $today;
2380 $params{'template'} = $template if $template;
2381 $params{$_} = $opt{$_}
2382 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2384 $template ||= $self->_agent_template;
2386 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2387 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2391 ) or die "can't open temp file: $!\n";
2393 my $agentnum = $self->cust_main->agentnum;
2395 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2396 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2397 or die "can't write temp file: $!\n";
2399 print $lh $conf->config_binary('logo.eps', $agentnum)
2400 or die "can't write temp file: $!\n";
2403 $params{'logo_file'} = $lh->filename;
2405 if($conf->exists('invoice-barcode')){
2406 my $png_file = $self->invoice_barcode($dir);
2407 my $eps_file = $png_file;
2408 $eps_file =~ s/\.png$/.eps/g;
2409 $png_file =~ /(barcode.*png)/;
2411 $eps_file =~ /(barcode.*eps)/;
2414 my $curr_dir = cwd();
2416 # after painfuly long experimentation, it was determined that sam2p won't
2417 # accept : and other chars in the path, no matter how hard I tried to
2418 # escape them, hence the chdir (and chdir back, just to be safe)
2419 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2420 or die "sam2p failed: $!\n";
2424 $params{'barcode_file'} = $eps_file;
2427 my @filled_in = $self->print_generic( %params );
2429 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2433 ) or die "can't open temp file: $!\n";
2434 binmode($fh, ':utf8'); # language support
2435 print $fh join('', @filled_in );
2438 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2439 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2443 =item invoice_barcode DIR_OR_FALSE
2445 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2446 it is taken as the temp directory where the PNG file will be generated and the
2447 PNG file name is returned. Otherwise, the PNG image itself is returned.
2451 sub invoice_barcode {
2452 my ($self, $dir) = (shift,shift);
2454 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2455 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2456 my $gd = $gdbar->plot(Height => 30);
2459 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2463 ) or die "can't open temp file: $!\n";
2464 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2465 my $png_file = $bh->filename;
2472 =item print_generic OPTION => VALUE ...
2474 Internal method - returns a filled-in template for this invoice as a scalar.
2476 See print_ps and print_pdf for methods that return PostScript and PDF output.
2478 Non optional options include
2479 format - latex, html, template
2481 Optional options include
2483 template - a value used as a suffix for a configuration template
2485 time - a value used to control the printing of overdue messages. The
2486 default is now. It isn't the date of the invoice; that's the `_date' field.
2487 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2488 L<Time::Local> and L<Date::Parse> for conversion functions.
2492 unsquelch_cdr - overrides any per customer cdr squelching when true
2494 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2496 locale - override customer's locale
2500 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2501 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2502 # yes: fixed width/plain text printing will be borked
2504 my( $self, %params ) = @_;
2505 my $conf = $self->conf;
2506 my $today = $params{today} ? $params{today} : time;
2507 warn "$me print_generic called on $self with suffix $params{template}\n"
2510 my $format = $params{format};
2511 die "Unknown format: $format"
2512 unless $format =~ /^(latex|html|template)$/;
2514 my $cust_main = $self->cust_main;
2515 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2516 unless $cust_main->payname
2517 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2519 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2520 'html' => [ '<%=', '%>' ],
2521 'template' => [ '{', '}' ],
2524 warn "$me print_generic creating template\n"
2527 #create the template
2528 my $template = $params{template} ? $params{template} : $self->_agent_template;
2529 my $templatefile = "invoice_$format";
2530 $templatefile .= "_$template"
2531 if length($template) && $conf->exists($templatefile."_$template");
2532 my @invoice_template = map "$_\n", $conf->config($templatefile)
2533 or die "cannot load config data $templatefile";
2536 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2537 #change this to a die when the old code is removed
2538 warn "old-style invoice template $templatefile; ".
2539 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2540 $old_latex = 'true';
2541 @invoice_template = _translate_old_latex_format(@invoice_template);
2544 warn "$me print_generic creating T:T object\n"
2547 my $text_template = new Text::Template(
2549 SOURCE => \@invoice_template,
2550 DELIMITERS => $delimiters{$format},
2553 warn "$me print_generic compiling T:T object\n"
2556 $text_template->compile()
2557 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2560 # additional substitution could possibly cause breakage in existing templates
2561 my %convert_maps = (
2563 'notes' => sub { map "$_", @_ },
2564 'footer' => sub { map "$_", @_ },
2565 'smallfooter' => sub { map "$_", @_ },
2566 'returnaddress' => sub { map "$_", @_ },
2567 'coupon' => sub { map "$_", @_ },
2568 'summary' => sub { map "$_", @_ },
2574 s/%%(.*)$/<!-- $1 -->/g;
2575 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2576 s/\\begin\{enumerate\}/<ol>/g;
2578 s/\\end\{enumerate\}/<\/ol>/g;
2579 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2588 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2590 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2595 s/\\\\\*?\s*$/<BR>/;
2596 s/\\hyphenation\{[\w\s\-]+}//;
2601 'coupon' => sub { "" },
2602 'summary' => sub { "" },
2609 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2610 s/\\begin\{enumerate\}//g;
2612 s/\\end\{enumerate\}//g;
2613 s/\\textbf\{(.*)\}/$1/g;
2620 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2622 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2627 s/\\\\\*?\s*$/\n/; # dubious
2628 s/\\hyphenation\{[\w\s\-]+}//;
2632 'coupon' => sub { "" },
2633 'summary' => sub { "" },
2638 # hashes for differing output formats
2639 my %nbsps = ( 'latex' => '~',
2640 'html' => '', # '&nbps;' would be nice
2641 'template' => '', # not used
2643 my $nbsp = $nbsps{$format};
2645 my %escape_functions = ( 'latex' => \&_latex_escape,
2646 'html' => \&_html_escape_nbsp,#\&encode_entities,
2647 'template' => sub { shift },
2649 my $escape_function = $escape_functions{$format};
2650 my $escape_function_nonbsp = ($format eq 'html')
2651 ? \&_html_escape : $escape_function;
2653 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2655 'html' => sub { return '<b>'. shift(). '</b>'
2657 'template' => sub { shift },
2659 my $embolden_function = $embolden_functions{$format};
2661 my %newline_tokens = ( 'latex' => '\\\\',
2665 my $newline_token = $newline_tokens{$format};
2667 warn "$me generating template variables\n"
2670 # generate template variables
2673 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2677 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2683 $returnaddress = join("\n",
2684 $conf->config_orbase("invoice_${format}returnaddress", $template)
2687 } elsif ( grep /\S/,
2688 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2690 my $convert_map = $convert_maps{$format}{'returnaddress'};
2693 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2698 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2700 my $convert_map = $convert_maps{$format}{'returnaddress'};
2701 $returnaddress = join( "\n", &$convert_map(
2702 map { s/( {2,})/'~' x length($1)/eg;
2706 ( $conf->config('company_name', $self->cust_main->agentnum),
2707 $conf->config('company_address', $self->cust_main->agentnum),
2714 my $warning = "Couldn't find a return address; ".
2715 "do you need to set the company_address configuration value?";
2717 $returnaddress = $nbsp;
2718 #$returnaddress = $warning;
2722 warn "$me generating invoice data\n"
2725 my $agentnum = $self->cust_main->agentnum;
2727 my %invoice_data = (
2730 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2731 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2732 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2733 'returnaddress' => $returnaddress,
2734 'agent' => &$escape_function($cust_main->agent->agent),
2737 'invnum' => $self->invnum,
2738 '_date' => $self->_date,
2739 'date' => $self->time2str_local('long', $self->_date, $format),
2740 'today' => $self->time2str_local('long', $today, $format),
2741 'terms' => $self->terms,
2742 'template' => $template, #params{'template'},
2743 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2744 'current_charges' => sprintf("%.2f", $self->charged),
2745 'duedate' => $self->due_date2str('rdate'),
2748 'custnum' => $cust_main->display_custnum,
2749 'agent_custid' => &$escape_function($cust_main->agent_custid),
2750 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2751 payname company address1 address2 city state zip fax
2755 'ship_enable' => $conf->exists('invoice-ship_address'),
2756 'unitprices' => $conf->exists('invoice-unitprice'),
2757 'smallernotes' => $conf->exists('invoice-smallernotes'),
2758 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2759 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2761 #layout info -- would be fancy to calc some of this and bury the template
2763 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2764 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2765 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2766 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2767 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2768 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2769 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2770 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2771 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2772 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2774 # better hang on to conf_dir for a while (for old templates)
2775 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2777 #these are only used when doing paged plaintext
2783 #localization (see FS::cust_main_Mixin)
2784 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2785 # prototype here to silence warnings
2786 $invoice_data{'time2str'} = sub ($;$$) { $self->time2str_local(@_, $format) };
2788 my $min_sdate = 999999999999;
2790 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2791 next unless $cust_bill_pkg->pkgnum > 0;
2792 $min_sdate = $cust_bill_pkg->sdate
2793 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2794 $max_edate = $cust_bill_pkg->edate
2795 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2798 $invoice_data{'bill_period'} = '';
2799 $invoice_data{'bill_period'} =
2800 $self->time2str_local('%e %h', $min_sdate, $format)
2802 $self->time2str_local('%e %h', $max_edate, $format)
2803 if ($max_edate != 0 && $min_sdate != 999999999999);
2805 $invoice_data{finance_section} = '';
2806 if ( $conf->config('finance_pkgclass') ) {
2808 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2809 $invoice_data{finance_section} = $pkg_class->categoryname;
2811 $invoice_data{finance_amount} = '0.00';
2812 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2814 my $countrydefault = $conf->config('countrydefault') || 'US';
2815 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2816 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2817 my $method = $prefix.$_;
2818 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2820 $invoice_data{'ship_country'} = ''
2821 if ( $invoice_data{'ship_country'} eq $countrydefault );
2823 $invoice_data{'cid'} = $params{'cid'}
2826 if ( $cust_main->country eq $countrydefault ) {
2827 $invoice_data{'country'} = '';
2829 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2833 $invoice_data{'address'} = \@address;
2835 $cust_main->payname.
2836 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2837 ? " (P.O. #". $cust_main->payinfo. ")"
2841 push @address, $cust_main->company
2842 if $cust_main->company;
2843 push @address, $cust_main->address1;
2844 push @address, $cust_main->address2
2845 if $cust_main->address2;
2847 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2848 push @address, $invoice_data{'country'}
2849 if $invoice_data{'country'};
2851 while (scalar(@address) < 5);
2853 $invoice_data{'logo_file'} = $params{'logo_file'}
2854 if $params{'logo_file'};
2855 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2856 if $params{'barcode_file'};
2857 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2858 if $params{'barcode_img'};
2859 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2860 if $params{'barcode_cid'};
2862 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2863 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2864 #my $balance_due = $self->owed + $pr_total - $cr_total;
2865 my $balance_due = $self->owed + $pr_total;
2867 # the customer's current balance as shown on the invoice before this one
2868 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2870 # the change in balance from that invoice to this one
2871 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2873 # the sum of amount owed on all previous invoices
2874 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2876 # the sum of amount owed on all invoices
2877 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2879 # info from customer's last invoice before this one, for some
2881 $invoice_data{'last_bill'} = {};
2882 if ( $self->previous_bill ) {
2883 $invoice_data{'last_bill'} = {
2884 '_date' => $self->previous_bill->_date, #unformatted
2885 # all we need for now
2889 my $summarypage = '';
2890 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2893 $invoice_data{'summarypage'} = $summarypage;
2895 warn "$me substituting variables in notes, footer, smallfooter\n"
2898 my @include = (qw( notes footer smallfooter ));
2899 push @include, 'coupon' unless $params{'no_coupon'};
2900 foreach my $include (@include) {
2902 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2905 if ( $conf->exists($inc_file, $agentnum)
2906 && length( $conf->config($inc_file, $agentnum) ) ) {
2908 @inc_src = $conf->config($inc_file, $agentnum);
2912 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2914 my $convert_map = $convert_maps{$format}{$include};
2916 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2917 s/--\@\]/$delimiters{$format}[1]/g;
2920 &$convert_map( $conf->config($inc_file, $agentnum) );
2924 my $inc_tt = new Text::Template (
2926 SOURCE => [ map "$_\n", @inc_src ],
2927 DELIMITERS => $delimiters{$format},
2928 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2930 unless ( $inc_tt->compile() ) {
2931 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2932 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2936 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2938 $invoice_data{$include} =~ s/\n+$//
2939 if ($format eq 'latex');
2942 # let invoices use either of these as needed
2943 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2944 ? $cust_main->payinfo : '';
2945 $invoice_data{'po_line'} =
2946 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2947 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2950 my %money_chars = ( 'latex' => '',
2951 'html' => $conf->config('money_char') || '$',
2954 my $money_char = $money_chars{$format};
2956 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2957 'html' => $conf->config('money_char') || '$',
2960 my $other_money_char = $other_money_chars{$format};
2961 $invoice_data{'dollar'} = $other_money_char;
2963 my @detail_items = ();
2964 my @total_items = ();
2968 $invoice_data{'detail_items'} = \@detail_items;
2969 $invoice_data{'total_items'} = \@total_items;
2970 $invoice_data{'buf'} = \@buf;
2971 $invoice_data{'sections'} = \@sections;
2973 warn "$me generating sections\n"
2976 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2977 'subtotal' => $other_money_char.
2978 sprintf('%.2f', $pr_total),
2979 'summarized' => '', #why? $summarypage ? 'Y' : '',
2981 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2982 join(' / ', map { $cust_main->balance_date_range(@$_) }
2983 $self->_prior_month30s
2985 if $conf->exists('invoice_include_aging');
2988 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2989 'subtotal' => $taxtotal, # adjusted below
2992 my $tax_weight = _pkg_category($tax_section->{description})
2993 ? _pkg_category($tax_section->{description})->weight
2995 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2996 $tax_section->{'sort_weight'} = $tax_weight;
2999 my $adjusttotal = 0;
3000 my $adjust_section = {
3001 'description' => $self->mt('Credits, Payments, and Adjustments'),
3002 'adjust_section' => 1,
3003 'subtotal' => 0, # adjusted below
3005 my $adjust_weight = _pkg_category($adjust_section->{description})
3006 ? _pkg_category($adjust_section->{description})->weight
3008 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
3009 $adjust_section->{'sort_weight'} = $adjust_weight;
3011 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3012 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3013 $invoice_data{'multisection'} = $multisection;
3014 my $late_sections = [];
3015 my $extra_sections = [];
3016 my $extra_lines = ();
3018 my $default_section = { 'description' => '',
3023 if ( $multisection ) {
3024 ($extra_sections, $extra_lines) =
3025 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3026 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3028 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3030 push @detail_items, @$extra_lines if $extra_lines;
3032 $self->_items_sections( $late_sections, # this could stand a refactor
3034 $escape_function_nonbsp,
3038 if ($conf->exists('svc_phone_sections')) {
3039 my ($phone_sections, $phone_lines) =
3040 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3041 push @{$late_sections}, @$phone_sections;
3042 push @detail_items, @$phone_lines;
3044 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3045 my ($accountcode_section, $accountcode_lines) =
3046 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3047 if ( scalar(@$accountcode_lines) ) {
3048 push @{$late_sections}, $accountcode_section;
3049 push @detail_items, @$accountcode_lines;
3052 } else {# not multisection
3053 # make a default section
3054 push @sections, $default_section;
3055 # and calculate the finance charge total, since it won't get done otherwise.
3056 # XXX possibly other totals?
3057 # XXX possibly finance_pkgclass should not be used in this manner?
3058 if ( $conf->exists('finance_pkgclass') ) {
3059 my @finance_charges;
3060 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3061 if ( grep { $_->section eq $invoice_data{finance_section} }
3062 $cust_bill_pkg->cust_bill_pkg_display ) {
3063 # I think these are always setup fees, but just to be sure...
3064 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3067 $invoice_data{finance_amount} =
3068 sprintf('%.2f', sum( @finance_charges ) || 0);
3072 # previous invoice balances in the Previous Charges section if there
3073 # is one, otherwise in the main detail section
3074 if ( $self->can('_items_previous') &&
3075 $self->enable_previous &&
3076 ! $conf->exists('previous_balance-summary_only') ) {
3078 warn "$me adding previous balances\n"
3081 foreach my $line_item ( $self->_items_previous ) {
3084 ext_description => [],
3086 $detail->{'ref'} = $line_item->{'pkgnum'};
3087 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3088 $detail->{'quantity'} = 1;
3089 $detail->{'section'} = $multisection ? $previous_section
3091 $detail->{'description'} = &$escape_function($line_item->{'description'});
3092 if ( exists $line_item->{'ext_description'} ) {
3093 @{$detail->{'ext_description'}} = map {
3094 &$escape_function($_);
3095 } @{$line_item->{'ext_description'}};
3097 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3098 $line_item->{'amount'};
3099 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3101 push @detail_items, $detail;
3102 push @buf, [ $detail->{'description'},
3103 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3109 if ( @pr_cust_bill && $self->enable_previous ) {
3110 push @buf, ['','-----------'];
3111 push @buf, [ $self->mt('Total Previous Balance'),
3112 $money_char. sprintf("%10.2f", $pr_total) ];
3116 if ( $conf->exists('svc_phone-did-summary') ) {
3117 warn "$me adding DID summary\n"
3120 my ($didsummary,$minutes) = $self->_did_summary;
3121 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3123 { 'description' => $didsummary_desc,
3124 'ext_description' => [ $didsummary, $minutes ],
3128 foreach my $section (@sections, @$late_sections) {
3130 warn "$me adding section \n". Dumper($section)
3133 # begin some normalization
3134 $section->{'subtotal'} = $section->{'amount'}
3136 && !exists($section->{subtotal})
3137 && exists($section->{amount});
3139 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3140 if ( $invoice_data{finance_section} &&
3141 $section->{'description'} eq $invoice_data{finance_section} );
3143 $section->{'subtotal'} = $other_money_char.
3144 sprintf('%.2f', $section->{'subtotal'})
3147 # continue some normalization
3148 $section->{'amount'} = $section->{'subtotal'}
3152 if ( $section->{'description'} ) {
3153 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3158 warn "$me setting options\n"
3161 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3163 $options{'section'} = $section if $multisection;
3164 $options{'format'} = $format;
3165 $options{'escape_function'} = $escape_function;
3166 $options{'no_usage'} = 1 unless $unsquelched;
3167 $options{'unsquelched'} = $unsquelched;
3168 $options{'summary_page'} = $summarypage;
3169 $options{'skip_usage'} =
3170 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3171 $options{'multilocation'} = $multilocation;
3172 $options{'multisection'} = $multisection;
3174 warn "$me searching for line items\n"
3177 foreach my $line_item ( $self->_items_pkg(%options) ) {
3179 warn "$me adding line item $line_item\n"
3183 ext_description => [],
3185 $detail->{'ref'} = $line_item->{'pkgnum'};
3186 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3187 $detail->{'quantity'} = $line_item->{'quantity'};
3188 $detail->{'section'} = $section;
3189 $detail->{'description'} = &$escape_function($line_item->{'description'});
3190 if ( exists $line_item->{'ext_description'} ) {
3191 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3193 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3194 $line_item->{'amount'};
3195 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3196 $line_item->{'unit_amount'};
3197 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3199 $detail->{'sdate'} = $line_item->{'sdate'};
3200 $detail->{'edate'} = $line_item->{'edate'};
3201 $detail->{'seconds'} = $line_item->{'seconds'};
3202 $detail->{'svc_label'} = $line_item->{'svc_label'};
3204 push @detail_items, $detail;
3205 push @buf, ( [ $detail->{'description'},
3206 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3208 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3212 if ( $section->{'description'} ) {
3213 push @buf, ( ['','-----------'],
3214 [ $section->{'description'}. ' sub-total',
3215 $section->{'subtotal'} # already formatted this
3224 $invoice_data{current_less_finance} =
3225 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3227 # create a major section for previous balance if we have major sections,
3228 # or if previous_section is in summary form
3229 if ( ( $multisection && $self->enable_previous )
3230 || $conf->exists('previous_balance-summary_only') )
3232 unshift @sections, $previous_section if $pr_total;
3235 warn "$me adding taxes\n"
3238 foreach my $tax ( $self->_items_tax ) {
3240 $taxtotal += $tax->{'amount'};
3242 my $description = &$escape_function( $tax->{'description'} );
3243 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3245 if ( $multisection ) {
3247 my $money = $old_latex ? '' : $money_char;
3248 push @detail_items, {
3249 ext_description => [],
3252 description => $description,
3253 amount => $money. $amount,
3255 section => $tax_section,
3260 push @total_items, {
3261 'total_item' => $description,
3262 'total_amount' => $other_money_char. $amount,
3267 push @buf,[ $description,
3268 $money_char. $amount,
3275 $total->{'total_item'} = $self->mt('Sub-total');
3276 $total->{'total_amount'} =
3277 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3279 if ( $multisection ) {
3280 $tax_section->{'subtotal'} = $other_money_char.
3281 sprintf('%.2f', $taxtotal);
3282 $tax_section->{'pretotal'} = 'New charges sub-total '.
3283 $total->{'total_amount'};
3284 push @sections, $tax_section if $taxtotal;
3286 unshift @total_items, $total;
3289 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3291 push @buf,['','-----------'];
3292 push @buf,[$self->mt(
3293 (!$self->enable_previous)
3295 : 'Total New Charges'
3297 $money_char. sprintf("%10.2f",$self->charged) ];
3300 # calculate total, possibly including total owed on previous
3305 $item = $conf->config('previous_balance-exclude_from_total')
3306 || 'Total New Charges'
3307 if $conf->exists('previous_balance-exclude_from_total');
3308 my $amount = $self->charged;
3309 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3310 $amount += $pr_total;
3313 $total->{'total_item'} = &$embolden_function($self->mt($item));
3314 $total->{'total_amount'} =
3315 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3316 if ( $multisection ) {
3317 if ( $adjust_section->{'sort_weight'} ) {
3318 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3319 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3321 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3322 $other_money_char. sprintf('%.2f', $self->charged );
3325 push @total_items, $total;
3327 push @buf,['','-----------'];
3330 sprintf( '%10.2f', $amount )
3335 # if we're showing previous invoices, also show previous
3336 # credits and payments
3337 if ( $self->enable_previous
3338 and $self->can('_items_credits')
3339 and $self->can('_items_payments') )
3341 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3344 my $credittotal = 0;
3345 foreach my $credit (
3346 $self->_items_credits( 'template' => $template, 'trim_len' => 60)
3350 $total->{'total_item'} = &$escape_function($credit->{'description'});
3351 $credittotal += $credit->{'amount'};
3352 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3353 $adjusttotal += $credit->{'amount'};
3354 if ( $multisection ) {
3355 my $money = $old_latex ? '' : $money_char;
3356 push @detail_items, {
3357 ext_description => [],
3360 description => &$escape_function($credit->{'description'}),
3361 amount => $money. $credit->{'amount'},
3363 section => $adjust_section,
3366 push @total_items, $total;
3370 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3373 foreach my $credit (
3374 $self->_items_credits( 'template' => $template, 'trim_len' => 32)
3376 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3380 my $paymenttotal = 0;
3381 foreach my $payment (
3382 $self->_items_payments( 'template' => $template )
3385 $total->{'total_item'} = &$escape_function($payment->{'description'});
3386 $paymenttotal += $payment->{'amount'};
3387 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3388 $adjusttotal += $payment->{'amount'};
3389 if ( $multisection ) {
3390 my $money = $old_latex ? '' : $money_char;
3391 push @detail_items, {
3392 ext_description => [],
3395 description => &$escape_function($payment->{'description'}),
3396 amount => $money. $payment->{'amount'},
3398 section => $adjust_section,
3401 push @total_items, $total;
3403 push @buf, [ $payment->{'description'},
3404 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3407 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3409 if ( $multisection ) {
3410 $adjust_section->{'subtotal'} = $other_money_char.
3411 sprintf('%.2f', $adjusttotal);
3412 push @sections, $adjust_section
3413 unless $adjust_section->{sort_weight};
3416 # create Balance Due message
3419 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3420 $total->{'total_amount'} =
3421 &$embolden_function(
3422 $other_money_char. sprintf('%.2f', #why? $summarypage
3423 # ? $self->charged +
3424 # $self->billing_balance
3426 $self->owed + $pr_total
3429 if ( $multisection && !$adjust_section->{sort_weight} ) {
3430 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3431 $total->{'total_amount'};
3433 push @total_items, $total;
3435 push @buf,['','-----------'];
3436 push @buf,[$self->balance_due_msg, $money_char.
3437 sprintf("%10.2f", $balance_due ) ];
3440 if ( $conf->exists('previous_balance-show_credit')
3441 and $cust_main->balance < 0 ) {
3442 my $credit_total = {
3443 'total_item' => &$embolden_function($self->credit_balance_msg),
3444 'total_amount' => &$embolden_function(
3445 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3448 if ( $multisection ) {
3449 $adjust_section->{'posttotal'} .= $newline_token .
3450 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3453 push @total_items, $credit_total;
3455 push @buf,['','-----------'];
3456 push @buf,[$self->credit_balance_msg, $money_char.
3457 sprintf("%10.2f", -$cust_main->balance ) ];
3461 if ( $multisection ) {
3462 if ($conf->exists('svc_phone_sections')) {
3464 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3465 $total->{'total_amount'} =
3466 &$embolden_function(
3467 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3469 my $last_section = pop @sections;
3470 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3471 $total->{'total_amount'};
3472 push @sections, $last_section;
3474 push @sections, @$late_sections
3478 # make a discounts-available section, even without multisection
3479 if ( $conf->exists('discount-show_available')
3480 and my @discounts_avail = $self->_items_discounts_avail ) {
3481 my $discount_section = {
3482 'description' => $self->mt('Discounts Available'),
3487 push @sections, $discount_section;
3488 push @detail_items, map { +{
3489 'ref' => '', #should this be something else?
3490 'section' => $discount_section,
3491 'description' => &$escape_function( $_->{description} ),
3492 'amount' => $money_char . &$escape_function( $_->{amount} ),
3493 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3494 } } @discounts_avail;
3497 # debugging hook: call this with 'diag' => 1 to just get a hash of
3498 # the invoice variables
3499 return \%invoice_data if ( $params{'diag'} );
3501 # All sections and items are built; now fill in templates.
3502 my @includelist = ();
3503 push @includelist, 'summary' if $summarypage;
3504 foreach my $include ( @includelist ) {
3506 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3509 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3511 @inc_src = $conf->config($inc_file, $agentnum);
3515 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3517 my $convert_map = $convert_maps{$format}{$include};
3519 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3520 s/--\@\]/$delimiters{$format}[1]/g;
3523 &$convert_map( $conf->config($inc_file, $agentnum) );
3527 my $inc_tt = new Text::Template (
3529 SOURCE => [ map "$_\n", @inc_src ],
3530 DELIMITERS => $delimiters{$format},
3531 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3533 unless ( $inc_tt->compile() ) {
3534 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3535 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3539 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3541 $invoice_data{$include} =~ s/\n+$//
3542 if ($format eq 'latex');
3547 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3548 /invoice_lines\((\d*)\)/;
3549 $invoice_lines += $1 || scalar(@buf);
3552 die "no invoice_lines() functions in template?"
3553 if ( $format eq 'template' && !$wasfunc );
3555 if ($format eq 'template') {
3557 if ( $invoice_lines ) {
3558 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3559 $invoice_data{'total_pages'}++
3560 if scalar(@buf) % $invoice_lines;
3563 #setup subroutine for the template
3564 $invoice_data{invoice_lines} = sub {
3565 my $lines = shift || scalar(@buf);
3577 push @collect, split("\n",
3578 $text_template->fill_in( HASH => \%invoice_data )
3580 $invoice_data{'page'}++;
3582 map "$_\n", @collect;
3584 # this is where we actually create the invoice
3585 warn "filling in template for invoice ". $self->invnum. "\n"
3587 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3590 $text_template->fill_in(HASH => \%invoice_data);
3594 # helper routine for generating date ranges
3595 sub _prior_month30s {
3598 [ 1, 2592000 ], # 0-30 days ago
3599 [ 2592000, 5184000 ], # 30-60 days ago
3600 [ 5184000, 7776000 ], # 60-90 days ago
3601 [ 7776000, 0 ], # 90+ days ago
3604 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3605 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3610 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3612 Returns an postscript invoice, as a scalar.
3614 Options can be passed as a hashref (recommended) or as a list of time, template
3615 and then any key/value pairs for any other options.
3617 I<time> an optional value used to control the printing of overdue messages. The
3618 default is now. It isn't the date of the invoice; that's the `_date' field.
3619 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3620 L<Time::Local> and L<Date::Parse> for conversion functions.
3622 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3629 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3630 my $ps = generate_ps($file);
3632 unlink($barcodefile) if $barcodefile;
3637 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3639 Returns an PDF invoice, as a scalar.
3641 Options can be passed as a hashref (recommended) or as a list of time, template
3642 and then any key/value pairs for any other options.
3644 I<time> an optional value used to control the printing of overdue messages. The
3645 default is now. It isn't the date of the invoice; that's the `_date' field.
3646 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3647 L<Time::Local> and L<Date::Parse> for conversion functions.
3649 I<template>, if specified, is the name of a suffix for alternate invoices.
3651 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3658 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3659 my $pdf = generate_pdf($file);
3661 unlink($barcodefile) if $barcodefile;
3666 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3668 Returns an HTML invoice, as a scalar.
3670 I<time> an optional value used to control the printing of overdue messages. The
3671 default is now. It isn't the date of the invoice; that's the `_date' field.
3672 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3673 L<Time::Local> and L<Date::Parse> for conversion functions.
3675 I<template>, if specified, is the name of a suffix for alternate invoices.
3677 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3679 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3680 when emailing the invoice as part of a multipart/related MIME email.
3688 %params = %{ shift() };
3690 $params{'time'} = shift;
3691 $params{'template'} = shift;
3692 $params{'cid'} = shift;
3695 $params{'format'} = 'html';
3697 $self->print_generic( %params );
3700 # quick subroutine for print_latex
3702 # There are ten characters that LaTeX treats as special characters, which
3703 # means that they do not simply typeset themselves:
3704 # # $ % & ~ _ ^ \ { }
3706 # TeX ignores blanks following an escaped character; if you want a blank (as
3707 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3711 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3712 $value =~ s/([<>])/\$$1\$/g;
3718 encode_entities($value);
3722 sub _html_escape_nbsp {
3723 my $value = _html_escape(shift);
3724 $value =~ s/ +/ /g;
3728 #utility methods for print_*
3730 sub _translate_old_latex_format {
3731 warn "_translate_old_latex_format called\n"
3738 if ( $line =~ /^%%Detail\s*$/ ) {
3740 push @template, q![@--!,
3741 q! foreach my $_tr_line (@detail_items) {!,
3742 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3743 q! $_tr_line->{'description'} .= !,
3744 q! "\\tabularnewline\n~~".!,
3745 q! join( "\\tabularnewline\n~~",!,
3746 q! @{$_tr_line->{'ext_description'}}!,
3750 while ( ( my $line_item_line = shift )
3751 !~ /^%%EndDetail\s*$/ ) {
3752 $line_item_line =~ s/'/\\'/g; # nice LTS
3753 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3754 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3755 push @template, " \$OUT .= '$line_item_line';";
3758 push @template, '}',
3761 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3763 push @template, '[@--',
3764 ' foreach my $_tr_line (@total_items) {';
3766 while ( ( my $total_item_line = shift )
3767 !~ /^%%EndTotalDetails\s*$/ ) {
3768 $total_item_line =~ s/'/\\'/g; # nice LTS
3769 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3770 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3771 push @template, " \$OUT .= '$total_item_line';";
3774 push @template, '}',
3778 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3779 push @template, $line;
3785 warn "$_\n" foreach @template;
3793 my $conf = $self->conf;
3795 #check for an invoice-specific override
3796 return $self->invoice_terms if $self->invoice_terms;
3798 #check for a customer- specific override
3799 my $cust_main = $self->cust_main;
3800 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3802 #use configured default
3803 $conf->config('invoice_default_terms') || '';
3809 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3810 $duedate = $self->_date() + ( $1 * 86400 );
3817 $self->due_date ? $self->time2str_local(shift, $self->due_date) : '';
3820 sub balance_due_msg {
3822 my $msg = $self->mt('Balance Due');
3823 return $msg unless $self->terms;
3824 if ( $self->due_date ) {
3825 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3826 $self->due_date2str('short');
3827 } elsif ( $self->terms ) {
3828 $msg .= ' - '. $self->terms;
3833 sub balance_due_date {
3835 my $conf = $self->conf;
3837 if ( $conf->exists('invoice_default_terms')
3838 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3839 $duedate = $self->time2str_local('rdate', $self->_date + ($1*86400) );
3844 sub credit_balance_msg {
3846 $self->mt('Credit Balance Remaining')
3849 =item invnum_date_pretty
3851 Returns a string with the invoice number and date, for example:
3852 "Invoice #54 (3/20/2008)"
3856 sub invnum_date_pretty {
3858 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3863 Returns a string with the date, for example: "3/20/2008"
3869 $self->time2str_local('short', $self->_date);
3872 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3874 Generate section information for all items appearing on this invoice.
3875 This will only be called for multi-section invoices.
3877 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3878 related display records (L<FS::cust_bill_pkg_display>) and organize
3879 them into two groups ("early" and "late" according to whether they come
3880 before or after the total), then into sections. A subtotal is calculated
3883 Section descriptions are returned in sort weight order. Each consists
3884 of a hash containing:
3886 description: the package category name, escaped
3887 subtotal: the total charges in that section
3888 tax_section: a flag indicating that the section contains only tax charges
3889 summarized: same as tax_section, for some reason
3890 sort_weight: the package category's sort weight
3892 If 'condense' is set on the display record, it also contains everything
3893 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3894 coderefs to generate parts of the invoice. This is not advised.
3898 LATE: an arrayref to push the "late" section hashes onto. The "early"
3899 group is simply returned from the method.
3901 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3902 Turning this on has the following effects:
3903 - Ignores display items with the 'summary' flag.
3904 - Combines all items into the "early" group.
3905 - Creates sections for all non-disabled package categories, even if they
3906 have no charges on this invoice, as well as a section with no name.
3908 ESCAPE: an escape function to use for section titles.
3910 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3911 sorted list. If there are any of these, section subtotals exclude
3914 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3915 passed through to C<_condense_section()>.
3919 use vars qw(%pkg_category_cache);
3920 sub _items_sections {
3923 my $summarypage = shift;
3925 my $extra_sections = shift;
3929 my %late_subtotal = ();
3932 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3935 my $usage = $cust_bill_pkg->usage;
3937 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3938 next if ( $display->summary && $summarypage );
3940 my $section = $display->section;
3941 my $type = $display->type;
3943 $not_tax{$section} = 1
3944 unless $cust_bill_pkg->pkgnum == 0;
3946 if ( $display->post_total && !$summarypage ) {
3947 if (! $type || $type eq 'S') {
3948 $late_subtotal{$section} += $cust_bill_pkg->setup
3949 if $cust_bill_pkg->setup != 0
3950 || $cust_bill_pkg->setup_show_zero;
3954 $late_subtotal{$section} += $cust_bill_pkg->recur
3955 if $cust_bill_pkg->recur != 0
3956 || $cust_bill_pkg->recur_show_zero;
3959 if ($type && $type eq 'R') {
3960 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3961 if $cust_bill_pkg->recur != 0
3962 || $cust_bill_pkg->recur_show_zero;
3965 if ($type && $type eq 'U') {
3966 $late_subtotal{$section} += $usage
3967 unless scalar(@$extra_sections);
3972 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3974 if (! $type || $type eq 'S') {
3975 $subtotal{$section} += $cust_bill_pkg->setup
3976 if $cust_bill_pkg->setup != 0
3977 || $cust_bill_pkg->setup_show_zero;
3981 $subtotal{$section} += $cust_bill_pkg->recur
3982 if $cust_bill_pkg->recur != 0
3983 || $cust_bill_pkg->recur_show_zero;
3986 if ($type && $type eq 'R') {
3987 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3988 if $cust_bill_pkg->recur != 0
3989 || $cust_bill_pkg->recur_show_zero;
3992 if ($type && $type eq 'U') {
3993 $subtotal{$section} += $usage
3994 unless scalar(@$extra_sections);
4003 %pkg_category_cache = ();
4005 push @$late, map { { 'description' => &{$escape}($_),
4006 'subtotal' => $late_subtotal{$_},
4008 'sort_weight' => ( _pkg_category($_)
4009 ? _pkg_category($_)->weight
4012 ((_pkg_category($_) && _pkg_category($_)->condense)
4013 ? $self->_condense_section($format)
4017 sort _sectionsort keys %late_subtotal;
4020 if ( $summarypage ) {
4021 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4022 map { $_->categoryname } qsearch('pkg_category', {});
4023 push @sections, '' if exists($subtotal{''});
4025 @sections = keys %subtotal;
4028 my @early = map { { 'description' => &{$escape}($_),
4029 'subtotal' => $subtotal{$_},
4030 'summarized' => $not_tax{$_} ? '' : 'Y',
4031 'tax_section' => $not_tax{$_} ? '' : 'Y',
4032 'sort_weight' => ( _pkg_category($_)
4033 ? _pkg_category($_)->weight
4036 ((_pkg_category($_) && _pkg_category($_)->condense)
4037 ? $self->_condense_section($format)
4042 push @early, @$extra_sections if $extra_sections;
4044 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4048 #helper subs for above
4051 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4055 my $categoryname = shift;
4056 $pkg_category_cache{$categoryname} ||=
4057 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4060 my %condensed_format = (
4061 'label' => [ qw( Description Qty Amount ) ],
4063 sub { shift->{description} },
4064 sub { shift->{quantity} },
4065 sub { my($href, %opt) = @_;
4066 ($opt{dollar} || ''). $href->{amount};
4069 'align' => [ qw( l r r ) ],
4070 'span' => [ qw( 5 1 1 ) ], # unitprices?
4071 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4074 sub _condense_section {
4075 my ( $self, $format ) = ( shift, shift );
4077 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4078 qw( description_generator
4081 total_line_generator
4086 sub _condensed_generator_defaults {
4087 my ( $self, $format ) = ( shift, shift );
4088 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4097 sub _condensed_header_generator {
4098 my ( $self, $format ) = ( shift, shift );
4100 my ( $f, $prefix, $suffix, $separator, $column ) =
4101 _condensed_generator_defaults($format);
4103 if ($format eq 'latex') {
4104 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4105 $suffix = "\\\\\n\\hline";
4108 sub { my ($d,$a,$s,$w) = @_;
4109 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4111 } elsif ( $format eq 'html' ) {
4112 $prefix = '<th></th>';
4116 sub { my ($d,$a,$s,$w) = @_;
4117 return qq!<th align="$html_align{$a}">$d</th>!;
4125 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4127 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4130 $prefix. join($separator, @result). $suffix;
4135 sub _condensed_description_generator {
4136 my ( $self, $format ) = ( shift, shift );
4138 my ( $f, $prefix, $suffix, $separator, $column ) =
4139 _condensed_generator_defaults($format);
4141 my $money_char = '$';
4142 if ($format eq 'latex') {
4143 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4145 $separator = " & \n";
4147 sub { my ($d,$a,$s,$w) = @_;
4148 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4150 $money_char = '\\dollar';
4151 }elsif ( $format eq 'html' ) {
4152 $prefix = '"><td align="center"></td>';
4156 sub { my ($d,$a,$s,$w) = @_;
4157 return qq!<td align="$html_align{$a}">$d</td>!;
4159 #$money_char = $conf->config('money_char') || '$';
4160 $money_char = ''; # this is madness
4168 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4170 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4172 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4173 map { $f->{$_}->[$i] } qw(align span width)
4177 $prefix. join( $separator, @result ). $suffix;
4182 sub _condensed_total_generator {
4183 my ( $self, $format ) = ( shift, shift );
4185 my ( $f, $prefix, $suffix, $separator, $column ) =
4186 _condensed_generator_defaults($format);
4189 if ($format eq 'latex') {
4192 $separator = " & \n";
4194 sub { my ($d,$a,$s,$w) = @_;
4195 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4197 }elsif ( $format eq 'html' ) {
4201 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4203 sub { my ($d,$a,$s,$w) = @_;
4204 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4213 # my $r = &{$f->{fields}->[$i]}(@args);
4214 # $r .= ' Total' unless $i;
4216 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4218 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4219 map { $f->{$_}->[$i] } qw(align span width)
4223 $prefix. join( $separator, @result ). $suffix;
4228 =item total_line_generator FORMAT
4230 Returns a coderef used for generation of invoice total line items for this
4231 usage_class. FORMAT is either html or latex
4235 # should not be used: will have issues with hash element names (description vs
4236 # total_item and amount vs total_amount -- another array of functions?
4238 sub _condensed_total_line_generator {
4239 my ( $self, $format ) = ( shift, shift );
4241 my ( $f, $prefix, $suffix, $separator, $column ) =
4242 _condensed_generator_defaults($format);
4245 if ($format eq 'latex') {
4248 $separator = " & \n";
4250 sub { my ($d,$a,$s,$w) = @_;
4251 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4253 }elsif ( $format eq 'html' ) {
4257 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4259 sub { my ($d,$a,$s,$w) = @_;
4260 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4269 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4271 &{$column}( &{$f->{fields}->[$i]}(@args),
4272 map { $f->{$_}->[$i] } qw(align span width)
4276 $prefix. join( $separator, @result ). $suffix;
4281 #sub _items_extra_usage_sections {
4283 # my $escape = shift;
4285 # my %sections = ();
4287 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4288 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4290 # next unless $cust_bill_pkg->pkgnum > 0;
4292 # foreach my $section ( keys %usage_class ) {
4294 # my $usage = $cust_bill_pkg->usage($section);
4296 # next unless $usage && $usage > 0;
4298 # $sections{$section} ||= 0;
4299 # $sections{$section} += $usage;
4305 # map { { 'description' => &{$escape}($_),
4306 # 'subtotal' => $sections{$_},
4307 # 'summarized' => '',
4308 # 'tax_section' => '',
4311 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4315 sub _items_extra_usage_sections {
4317 my $conf = $self->conf;
4325 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4327 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4328 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4329 next unless $cust_bill_pkg->pkgnum > 0;
4331 foreach my $classnum ( keys %usage_class ) {
4332 my $section = $usage_class{$classnum}->classname;
4333 $classnums{$section} = $classnum;
4335 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4336 my $amount = $detail->amount;
4337 next unless $amount && $amount > 0;
4339 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4340 $sections{$section}{amount} += $amount; #subtotal
4341 $sections{$section}{calls}++;
4342 $sections{$section}{duration} += $detail->duration;
4344 my $desc = $detail->regionname;
4345 my $description = $desc;
4346 $description = substr($desc, 0, $maxlength). '...'
4347 if $format eq 'latex' && length($desc) > $maxlength;
4349 $lines{$section}{$desc} ||= {
4350 description => &{$escape}($description),
4351 #pkgpart => $part_pkg->pkgpart,
4352 pkgnum => $cust_bill_pkg->pkgnum,
4357 #unit_amount => $cust_bill_pkg->unitrecur,
4358 quantity => $cust_bill_pkg->quantity,
4359 product_code => 'N/A',
4360 ext_description => [],
4363 $lines{$section}{$desc}{amount} += $amount;
4364 $lines{$section}{$desc}{calls}++;
4365 $lines{$section}{$desc}{duration} += $detail->duration;
4371 my %sectionmap = ();
4372 foreach (keys %sections) {
4373 my $usage_class = $usage_class{$classnums{$_}};
4374 $sectionmap{$_} = { 'description' => &{$escape}($_),
4375 'amount' => $sections{$_}{amount}, #subtotal
4376 'calls' => $sections{$_}{calls},
4377 'duration' => $sections{$_}{duration},
4379 'tax_section' => '',
4380 'sort_weight' => $usage_class->weight,
4381 ( $usage_class->format
4382 ? ( map { $_ => $usage_class->$_($format) }
4383 qw( description_generator header_generator total_generator total_line_generator )
4390 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4394 foreach my $section ( keys %lines ) {
4395 foreach my $line ( keys %{$lines{$section}} ) {
4396 my $l = $lines{$section}{$line};
4397 $l->{section} = $sectionmap{$section};
4398 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4399 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4404 return(\@sections, \@lines);
4410 my $end = $self->_date;
4412 # start at date of previous invoice + 1 second or 0 if no previous invoice
4413 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4414 $start = 0 if !$start;
4417 my $cust_main = $self->cust_main;
4418 my @pkgs = $cust_main->all_pkgs;
4419 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4422 foreach my $pkg ( @pkgs ) {
4423 my @h_cust_svc = $pkg->h_cust_svc($end);
4424 foreach my $h_cust_svc ( @h_cust_svc ) {
4425 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4426 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4428 my $inserted = $h_cust_svc->date_inserted;
4429 my $deleted = $h_cust_svc->date_deleted;
4430 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4432 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4434 # DID either activated or ported in; cannot be both for same DID simultaneously
4435 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4436 && (!$phone_inserted->lnp_status
4437 || $phone_inserted->lnp_status eq ''
4438 || $phone_inserted->lnp_status eq 'native')) {
4441 else { # this one not so clean, should probably move to (h_)svc_phone
4442 my $phone_portedin = qsearchs( 'h_svc_phone',
4443 { 'svcnum' => $h_cust_svc->svcnum,
4444 'lnp_status' => 'portedin' },
4445 FS::h_svc_phone->sql_h_searchs($end),
4447 $num_portedin++ if $phone_portedin;
4450 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4451 if($deleted >= $start && $deleted <= $end && $phone_deleted
4452 && (!$phone_deleted->lnp_status
4453 || $phone_deleted->lnp_status ne 'portingout')) {
4456 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4457 && $phone_deleted->lnp_status
4458 && $phone_deleted->lnp_status eq 'portingout') {
4462 # increment usage minutes
4463 if ( $phone_inserted ) {
4464 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4465 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4468 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4471 # don't look at this service again
4472 push @seen, $h_cust_svc->svcnum;
4476 $minutes = sprintf("%d", $minutes);
4477 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4478 . "$num_deactivated Ported-Out: $num_portedout ",
4479 "Total Minutes: $minutes");
4482 sub _items_accountcode_cdr {
4487 my $section = { 'amount' => 0,
4490 'sort_weight' => '',
4492 'description' => 'Usage by Account Code',
4498 my %accountcodes = ();
4500 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4501 next unless $cust_bill_pkg->pkgnum > 0;
4503 my @header = $cust_bill_pkg->details_header;
4504 next unless scalar(@header);
4505 $section->{'header'} = join(',',@header);
4507 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4509 $section->{'header'} = $detail->formatted('format' => $format)
4510 if($detail->detail eq $section->{'header'});
4512 my $accountcode = $detail->accountcode;
4513 next unless $accountcode;
4515 my $amount = $detail->amount;
4516 next unless $amount && $amount > 0;
4518 $accountcodes{$accountcode} ||= {
4519 description => $accountcode,
4526 product_code => 'N/A',
4527 section => $section,
4528 ext_description => [ $section->{'header'} ],
4532 $section->{'amount'} += $amount;
4533 $accountcodes{$accountcode}{'amount'} += $amount;
4534 $accountcodes{$accountcode}{calls}++;
4535 $accountcodes{$accountcode}{duration} += $detail->duration;
4536 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4540 foreach my $l ( values %accountcodes ) {
4541 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4542 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4543 foreach my $sorted_detail ( @sorted_detail ) {
4544 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4546 delete $l->{detail_temp};
4550 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4552 return ($section,\@sorted_lines);
4555 sub _items_svc_phone_sections {
4557 my $conf = $self->conf;
4565 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4567 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4568 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4570 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4571 next unless $cust_bill_pkg->pkgnum > 0;
4573 my @header = $cust_bill_pkg->details_header;
4574 next unless scalar(@header);
4576 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4578 my $phonenum = $detail->phonenum;
4579 next unless $phonenum;
4581 my $amount = $detail->amount;
4582 next unless $amount && $amount > 0;
4584 $sections{$phonenum} ||= { 'amount' => 0,
4587 'sort_weight' => -1,
4588 'phonenum' => $phonenum,
4590 $sections{$phonenum}{amount} += $amount; #subtotal
4591 $sections{$phonenum}{calls}++;
4592 $sections{$phonenum}{duration} += $detail->duration;
4594 my $desc = $detail->regionname;
4595 my $description = $desc;
4596 $description = substr($desc, 0, $maxlength). '...'
4597 if $format eq 'latex' && length($desc) > $maxlength;
4599 $lines{$phonenum}{$desc} ||= {
4600 description => &{$escape}($description),
4601 #pkgpart => $part_pkg->pkgpart,
4609 product_code => 'N/A',
4610 ext_description => [],
4613 $lines{$phonenum}{$desc}{amount} += $amount;
4614 $lines{$phonenum}{$desc}{calls}++;
4615 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4617 my $line = $usage_class{$detail->classnum}->classname;
4618 $sections{"$phonenum $line"} ||=
4622 'sort_weight' => $usage_class{$detail->classnum}->weight,
4623 'phonenum' => $phonenum,
4624 'header' => [ @header ],
4626 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4627 $sections{"$phonenum $line"}{calls}++;
4628 $sections{"$phonenum $line"}{duration} += $detail->duration;
4630 $lines{"$phonenum $line"}{$desc} ||= {
4631 description => &{$escape}($description),
4632 #pkgpart => $part_pkg->pkgpart,
4640 product_code => 'N/A',
4641 ext_description => [],
4644 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4645 $lines{"$phonenum $line"}{$desc}{calls}++;
4646 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4647 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4648 $detail->formatted('format' => $format);
4653 my %sectionmap = ();
4654 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4655 foreach ( keys %sections ) {
4656 my @header = @{ $sections{$_}{header} || [] };
4658 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4659 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4660 my $usage_class = $summary ? $simple : $usage_simple;
4661 my $ending = $summary ? ' usage charges' : '';
4664 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4666 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4667 'amount' => $sections{$_}{amount}, #subtotal
4668 'calls' => $sections{$_}{calls},
4669 'duration' => $sections{$_}{duration},
4671 'tax_section' => '',
4672 'phonenum' => $sections{$_}{phonenum},
4673 'sort_weight' => $sections{$_}{sort_weight},
4674 'post_total' => $summary, #inspire pagebreak
4676 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4677 qw( description_generator
4680 total_line_generator
4687 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4688 $a->{sort_weight} <=> $b->{sort_weight}
4693 foreach my $section ( keys %lines ) {
4694 foreach my $line ( keys %{$lines{$section}} ) {
4695 my $l = $lines{$section}{$line};
4696 $l->{section} = $sectionmap{$section};
4697 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4698 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4703 if($conf->exists('phone_usage_class_summary')) {
4704 # this only works with Latex
4708 # after this, we'll have only two sections per DID:
4709 # Calls Summary and Calls Detail
4710 foreach my $section ( @sections ) {
4711 if($section->{'post_total'}) {
4712 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4713 $section->{'total_line_generator'} = sub { '' };
4714 $section->{'total_generator'} = sub { '' };
4715 $section->{'header_generator'} = sub { '' };
4716 $section->{'description_generator'} = '';
4717 push @newsections, $section;
4718 my %calls_detail = %$section;
4719 $calls_detail{'post_total'} = '';
4720 $calls_detail{'sort_weight'} = '';
4721 $calls_detail{'description_generator'} = sub { '' };
4722 $calls_detail{'header_generator'} = sub {
4723 return ' & Date/Time & Called Number & Duration & Price'
4724 if $format eq 'latex';
4727 $calls_detail{'description'} = 'Calls Detail: '
4728 . $section->{'phonenum'};
4729 push @newsections, \%calls_detail;
4733 # after this, each usage class is collapsed/summarized into a single
4734 # line under the Calls Summary section
4735 foreach my $newsection ( @newsections ) {
4736 if($newsection->{'post_total'}) { # this means Calls Summary
4737 foreach my $section ( @sections ) {
4738 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4739 && !$section->{'post_total'});
4740 my $newdesc = $section->{'description'};
4741 my $tn = $section->{'phonenum'};
4742 $newdesc =~ s/$tn//g;
4743 my $line = { ext_description => [],
4747 calls => $section->{'calls'},
4748 section => $newsection,
4749 duration => $section->{'duration'},
4750 description => $newdesc,
4751 amount => sprintf("%.2f",$section->{'amount'}),
4752 product_code => 'N/A',
4754 push @newlines, $line;
4759 # after this, Calls Details is populated with all CDRs
4760 foreach my $newsection ( @newsections ) {
4761 if(!$newsection->{'post_total'}) { # this means Calls Details
4762 foreach my $line ( @lines ) {
4763 next unless (scalar(@{$line->{'ext_description'}}) &&
4764 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4766 my @extdesc = @{$line->{'ext_description'}};
4768 foreach my $extdesc ( @extdesc ) {
4769 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4770 push @newextdesc, $extdesc;
4772 $line->{'ext_description'} = \@newextdesc;
4773 $line->{'section'} = $newsection;
4774 push @newlines, $line;
4779 return(\@newsections, \@newlines);
4782 return(\@sections, \@lines);
4786 sub _items { # seems to be unused
4789 #my @display = scalar(@_)
4791 # : qw( _items_previous _items_pkg );
4792 # #: qw( _items_pkg );
4793 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4794 my @display = qw( _items_previous _items_pkg );
4797 foreach my $display ( @display ) {
4798 push @b, $self->$display(@_);
4803 sub _items_previous {
4805 my $conf = $self->conf;
4806 my $cust_main = $self->cust_main;
4807 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4809 foreach ( @pr_cust_bill ) {
4810 my $date = $conf->exists('invoice_show_prior_due_date')
4811 ? 'due '. $_->due_date2str('short')
4812 : $self->time2str_local('short', $_->_date);
4814 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4815 #'pkgpart' => 'N/A',
4817 'amount' => sprintf("%.2f", $_->owed),
4823 # 'description' => 'Previous Balance',
4824 # #'pkgpart' => 'N/A',
4825 # 'pkgnum' => 'N/A',
4826 # 'amount' => sprintf("%10.2f", $pr_total ),
4827 # 'ext_description' => [ map {
4828 # "Invoice ". $_->invnum.
4829 # " (". time2str("%x",$_->_date). ") ".
4830 # sprintf("%10.2f", $_->owed)
4831 # } @pr_cust_bill ],
4836 =item _items_pkg [ OPTIONS ]
4838 Return line item hashes for each package item on this invoice. Nearly
4841 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4843 The only OPTIONS accepted is 'section', which may point to a hashref
4844 with a key named 'condensed', which may have a true value. If it
4845 does, this method tries to merge identical items into items with
4846 'quantity' equal to the number of items (not the sum of their
4847 separate quantities, for some reason).
4855 warn "$me _items_pkg searching for all package line items\n"
4858 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4860 warn "$me _items_pkg filtering line items\n"
4862 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4864 if ($options{section} && $options{section}->{condensed}) {
4866 warn "$me _items_pkg condensing section\n"
4870 local $Storable::canonical = 1;
4871 foreach ( @items ) {
4873 delete $item->{ref};
4874 delete $item->{ext_description};
4875 my $key = freeze($item);
4876 $itemshash{$key} ||= 0;
4877 $itemshash{$key} ++; # += $item->{quantity};
4879 @items = sort { $a->{description} cmp $b->{description} }
4880 map { my $i = thaw($_);
4881 $i->{quantity} = $itemshash{$_};
4883 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4889 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4896 return 0 unless $a->itemdesc cmp $b->itemdesc;
4897 return -1 if $b->itemdesc eq 'Tax';
4898 return 1 if $a->itemdesc eq 'Tax';
4899 return -1 if $b->itemdesc eq 'Other surcharges';
4900 return 1 if $a->itemdesc eq 'Other surcharges';
4901 $a->itemdesc cmp $b->itemdesc;
4906 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4907 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4910 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4912 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4913 list of hashrefs describing the line items they generate on the invoice.
4915 OPTIONS may include:
4917 format: the invoice format.
4919 escape_function: the function used to escape strings.
4921 DEPRECATED? (expensive, mostly unused?)
4922 format_function: the function used to format CDRs.
4924 section: a hashref containing 'description'; if this is present,
4925 cust_bill_pkg_display records not belonging to this section are
4928 multisection: a flag indicating that this is a multisection invoice,
4929 which does something complicated.
4931 multilocation: a flag to display the location label for the package.
4933 Returns a list of hashrefs, each of which may contain:
4935 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4936 ext_description, which is an arrayref of detail lines to show below
4941 sub _items_cust_bill_pkg {
4943 my $conf = $self->conf;
4944 my $cust_bill_pkgs = shift;
4947 my $format = $opt{format} || '';
4948 my $escape_function = $opt{escape_function} || sub { shift };
4949 my $format_function = $opt{format_function} || '';
4950 my $no_usage = $opt{no_usage} || '';
4951 my $unsquelched = $opt{unsquelched} || ''; #unused
4952 my $section = $opt{section}->{description} if $opt{section};
4953 my $summary_page = $opt{summary_page} || ''; #unused
4954 my $multilocation = $opt{multilocation} || '';
4955 my $multisection = $opt{multisection} || '';
4956 my $discount_show_always = 0;
4958 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4960 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4963 my ($s, $r, $u) = ( undef, undef, undef );
4964 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4967 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4968 if ( $_ && !$cust_bill_pkg->hidden ) {
4969 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4970 $_->{amount} =~ s/^\-0\.00$/0.00/;
4971 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4973 if $_->{amount} != 0
4974 || $discount_show_always
4975 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4976 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4982 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4984 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4985 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4988 foreach my $display ( grep { defined($section)
4989 ? $_->section eq $section
4992 #grep { !$_->summary || !$summary_page } # bunk!
4993 grep { !$_->summary || $multisection }
4994 @cust_bill_pkg_display
4998 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4999 $display->billpkgdisplaynum. "\n"
5002 my $type = $display->type;
5004 my $desc = $cust_bill_pkg->desc;
5005 $desc = substr($desc, 0, $maxlength). '...'
5006 if $format eq 'latex' && length($desc) > $maxlength;
5008 my %details_opt = ( 'format' => $format,
5009 'escape_function' => $escape_function,
5010 'format_function' => $format_function,
5011 'no_usage' => $opt{'no_usage'},
5014 if ( $cust_bill_pkg->pkgnum > 0 ) {
5016 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
5019 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5021 # which pkgpart to show for display purposes?
5022 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5024 # start/end dates for invoice formats that do nonstandard
5026 my %item_dates = ();
5027 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5028 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5030 if ( (!$type || $type eq 'S')
5031 && ( $cust_bill_pkg->setup != 0
5032 || $cust_bill_pkg->setup_show_zero
5037 warn "$me _items_cust_bill_pkg adding setup\n"
5040 my $description = $desc;
5041 $description .= ' Setup'
5042 if $cust_bill_pkg->recur != 0
5043 || $discount_show_always
5044 || $cust_bill_pkg->recur_show_zero;
5048 unless ( $cust_pkg->part_pkg->hide_svc_detail
5049 || $cust_bill_pkg->hidden )
5052 my @svc_labels = map &{$escape_function}($_),
5053 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5054 push @d, @svc_labels
5055 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5056 $svc_label = $svc_labels[0];
5058 if ( $multilocation ) {
5059 my $loc = $cust_pkg->location_label;
5060 $loc = substr($loc, 0, $maxlength). '...'
5061 if $format eq 'latex' && length($loc) > $maxlength;
5062 push @d, &{$escape_function}($loc);
5065 } #unless hiding service details
5067 push @d, $cust_bill_pkg->details(%details_opt)
5068 if $cust_bill_pkg->recur == 0;
5070 if ( $cust_bill_pkg->hidden ) {
5071 $s->{amount} += $cust_bill_pkg->setup;
5072 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5073 push @{ $s->{ext_description} }, @d;
5077 description => $description,
5078 pkgpart => $pkgpart,
5079 pkgnum => $cust_bill_pkg->pkgnum,
5080 amount => $cust_bill_pkg->setup,
5081 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5082 unit_amount => $cust_bill_pkg->unitsetup,
5083 quantity => $cust_bill_pkg->quantity,
5084 ext_description => \@d,
5085 svc_label => ($svc_label || ''),
5091 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5093 $cust_bill_pkg->recur != 0
5094 || $cust_bill_pkg->setup == 0
5095 || $discount_show_always
5096 || $cust_bill_pkg->recur_show_zero
5101 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5104 my $is_summary = $display->summary;
5105 my $description = ($is_summary && $type && $type eq 'U')
5106 ? "Usage charges" : $desc;
5108 my $part_pkg = $cust_pkg->part_pkg;
5110 #pry be a bit more efficient to look some of this conf stuff up
5113 $conf->exists('disable_line_item_date_ranges')
5114 || $part_pkg->option('disable_line_item_date_ranges',1)
5115 || ! $cust_bill_pkg->sdate
5116 || ! $cust_bill_pkg->edate
5119 my $date_style = '';
5120 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5121 $cust_main->agentnum
5123 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5124 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5125 $cust_main->agentnum
5127 if ( defined($date_style) && $date_style eq 'month_of' ) {
5128 $time_period = $self->mt('The month of [_1]',
5129 $self->time2str_local('%B', $cust_bill_pkg->sdate)
5131 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5132 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5133 $cust_main->agentnum
5135 $desc .= ' ' unless $desc =~ /\s$/;
5136 $time_period = $desc. $self->time2str_local('%B', $cust_bill_pkg->sdate);
5138 $time_period = $self->time2str_local('short', $cust_bill_pkg->sdate).
5139 " - ". $self->time2str_local('short', $cust_bill_pkg->edate);
5141 $description .= " ($time_period)";
5145 my @seconds = (); # for display of usage info
5148 #at least until cust_bill_pkg has "past" ranges in addition to
5149 #the "future" sdate/edate ones... see #3032
5150 my @dates = ( $self->_date );
5151 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5152 push @dates, $prev->sdate if $prev;
5153 push @dates, undef if !$prev;
5155 unless ( $cust_pkg->part_pkg->hide_svc_detail
5156 || $cust_bill_pkg->itemdesc
5157 || $cust_bill_pkg->hidden
5158 || $is_summary && $type && $type eq 'U' )
5161 warn "$me _items_cust_bill_pkg adding service details\n"
5164 my @svc_labels = map &{$escape_function}($_),
5165 $cust_pkg->h_labels_short(@dates, 'I');
5166 push @d, @svc_labels
5167 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5168 $svc_label = $svc_labels[0];
5170 warn "$me _items_cust_bill_pkg done adding service details\n"
5173 if ( $multilocation ) {
5174 my $loc = $cust_pkg->location_label;
5175 $loc = substr($loc, 0, $maxlength). '...'
5176 if $format eq 'latex' && length($loc) > $maxlength;
5177 push @d, &{$escape_function}($loc);
5180 # Display of seconds_since_sqlradacct:
5181 # On the invoice, when processing @detail_items, look for a field
5182 # named 'seconds'. This will contain total seconds for each
5183 # service, in the same order as @ext_description. For services
5184 # that don't support this it will show undef.
5185 if ( $conf->exists('svc_acct-usage_seconds')
5186 and ! $cust_bill_pkg->pkgpart_override ) {
5187 foreach my $cust_svc (
5188 $cust_pkg->h_cust_svc(@dates, 'I')
5191 # eval because not having any part_export_usage exports
5192 # is a fatal error, last_bill/_date because that's how
5193 # sqlradius_hour billing does it
5195 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5197 push @seconds, $sec;
5199 } #if svc_acct-usage_seconds
5203 unless ( $is_summary ) {
5204 warn "$me _items_cust_bill_pkg adding details\n"
5207 #instead of omitting details entirely in this case (unwanted side
5208 # effects), just omit CDRs
5209 $details_opt{'no_usage'} = 1
5210 if $type && $type eq 'R';
5212 push @d, $cust_bill_pkg->details(%details_opt);
5215 warn "$me _items_cust_bill_pkg calculating amount\n"
5220 $amount = $cust_bill_pkg->recur;
5221 } elsif ($type eq 'R') {
5222 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5223 } elsif ($type eq 'U') {
5224 $amount = $cust_bill_pkg->usage;
5227 if ( !$type || $type eq 'R' ) {
5229 warn "$me _items_cust_bill_pkg adding recur\n"
5232 if ( $cust_bill_pkg->hidden ) {
5233 $r->{amount} += $amount;
5234 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5235 push @{ $r->{ext_description} }, @d;
5238 description => $description,
5239 pkgpart => $pkgpart,
5240 pkgnum => $cust_bill_pkg->pkgnum,
5242 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5243 unit_amount => $cust_bill_pkg->unitrecur,
5244 quantity => $cust_bill_pkg->quantity,
5246 ext_description => \@d,
5247 svc_label => ($svc_label || ''),
5249 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5252 } else { # $type eq 'U'
5254 warn "$me _items_cust_bill_pkg adding usage\n"
5257 if ( $cust_bill_pkg->hidden ) {
5258 $u->{amount} += $amount;
5259 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5260 push @{ $u->{ext_description} }, @d;
5263 description => $description,
5264 pkgpart => $pkgpart,
5265 pkgnum => $cust_bill_pkg->pkgnum,
5267 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5268 unit_amount => $cust_bill_pkg->unitrecur,
5269 quantity => $cust_bill_pkg->quantity,
5271 ext_description => \@d,
5276 } # recurring or usage with recurring charge
5278 } else { #pkgnum tax or one-shot line item (??)
5280 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5283 if ( $cust_bill_pkg->setup != 0 ) {
5285 'description' => $desc,
5286 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5289 if ( $cust_bill_pkg->recur != 0 ) {
5291 'description' => "$desc (".
5292 $self->time2str_local('short', $cust_bill_pkg->sdate). ' - '.
5293 $self->time2str_local('short', $cust_bill_pkg->edate). ')',
5294 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5302 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5303 && $conf->exists('discount-show-always'));
5307 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5309 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5310 $_->{amount} =~ s/^\-0\.00$/0.00/;
5311 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5313 if $_->{amount} != 0
5314 || $discount_show_always
5315 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5316 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5320 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5327 sub _items_credits {
5328 my( $self, %opt ) = @_;
5329 my $trim_len = $opt{'trim_len'} || 60;
5334 if ( $self->conf->exists('previous_balance-payments_since') ) {
5335 if ( $opt{'template'} eq 'statement' ) {
5336 # then the current bill is a "statement" (i.e. an invoice sent as
5337 # a payment receipt)
5338 # and in that case we want to see payments on or after THIS invoice
5339 @objects = qsearch('cust_credit', {
5340 'custnum' => $self->custnum,
5341 '_date' => {op => '>=', value => $self->_date},
5345 $date = $self->previous_bill->_date if $self->previous_bill;
5346 @objects = qsearch('cust_credit', {
5347 'custnum' => $self->custnum,
5348 '_date' => {op => '>=', value => $date},
5352 @objects = $self->cust_credited;
5355 foreach my $obj ( @objects ) {
5356 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
5358 my $reason = substr($cust_credit->reason, 0, $trim_len);
5359 $reason .= '...' if length($reason) < length($cust_credit->reason);
5360 $reason = " ($reason) " if $reason;
5363 #'description' => 'Credit ref\#'. $_->crednum.
5364 # " (". time2str("%x",$_->cust_credit->_date) .")".
5366 'description' => $self->mt('Credit applied').' '.
5367 $self->time2str_local('short', $obj->_date). $reason,
5368 'amount' => sprintf("%.2f",$obj->amount),
5376 sub _items_payments {
5381 my $detailed = $self->conf->exists('invoice_payment_details');
5383 if ( $self->conf->exists('previous_balance-payments_since') ) {
5384 # then show payments dated on/after the previous bill...
5385 if ( $opt{'template'} eq 'statement' ) {
5386 # then the current bill is a "statement" (i.e. an invoice sent as
5387 # a payment receipt)
5388 # and in that case we want to see payments on or after THIS invoice
5389 @objects = qsearch('cust_pay', {
5390 'custnum' => $self->custnum,
5391 '_date' => {op => '>=', value => $self->_date},
5394 # the normal case: payments on or after the previous invoice
5396 $date = $self->previous_bill->_date if $self->previous_bill;
5397 @objects = qsearch('cust_pay', {
5398 'custnum' => $self->custnum,
5399 '_date' => {op => '>=', value => $date},
5401 # and before the current bill...
5402 @objects = grep { $_->_date < $self->_date } @objects;
5405 @objects = $self->cust_bill_pay;
5408 foreach my $obj (@objects) {
5409 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
5410 my $desc = $self->mt('Payment received').' '.
5411 $self->time2str_local('short', $cust_pay->_date );
5412 $desc .= $self->mt(' via ') .
5413 $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
5417 'description' => $desc,
5418 'amount' => sprintf("%.2f", $obj->amount )
5426 =item _items_discounts_avail
5428 Returns an array of line item hashrefs representing available term discounts
5429 for this invoice. This makes the same assumptions that apply to term
5430 discounts in general: that the package is billed monthly, at a flat rate,
5431 with no usage charges. A prorated first month will be handled, as will
5432 a setup fee if the discount is allowed to apply to setup fees.
5436 sub _items_discounts_avail {
5438 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5440 my %plans = $self->discount_plans;
5442 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5446 my $plan = $plans{$months};
5448 my $term_total = sprintf('%.2f', $plan->discounted_total);
5449 my $percent = sprintf('%.0f',
5450 100 * (1 - $term_total / $plan->base_total) );
5451 my $permonth = sprintf('%.2f', $term_total / $months);
5452 my $detail = $self->mt('discount on item'). ' '.
5453 join(', ', map { "#$_" } $plan->pkgnums)
5456 # discounts for non-integer months don't work anyway
5457 $months = sprintf("%d", $months);
5460 description => $self->mt('Save [_1]% by paying for [_2] months',
5462 amount => $self->mt('[_1] ([_2] per month)',
5463 $term_total, $money_char.$permonth),
5464 ext_description => ($detail || ''),
5467 sort { $b <=> $a } keys %plans;
5471 =item call_details [ OPTION => VALUE ... ]
5473 Returns an array of CSV strings representing the call details for this invoice
5474 The only option available is the boolean prepend_billed_number
5479 my ($self, %opt) = @_;
5481 my $format_function = sub { shift };
5483 if ($opt{prepend_billed_number}) {
5484 $format_function = sub {
5488 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5493 my @details = map { $_->details( 'format_function' => $format_function,
5494 'escape_function' => sub{ return() },
5498 $self->cust_bill_pkg;
5499 my $header = $details[0];
5500 ( $header, grep { $_ ne $header } @details );
5510 =item process_reprint
5514 sub process_reprint {
5515 process_re_X('print', @_);
5518 =item process_reemail
5522 sub process_reemail {
5523 process_re_X('email', @_);
5531 process_re_X('fax', @_);
5539 process_re_X('ftp', @_);
5546 sub process_respool {
5547 process_re_X('spool', @_);
5550 use Storable qw(thaw);
5554 my( $method, $job ) = ( shift, shift );
5555 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5557 my $param = thaw(decode_base64(shift));
5558 warn Dumper($param) if $DEBUG;
5569 my($method, $job, %param ) = @_;
5571 warn "re_X $method for job $job with param:\n".
5572 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5575 #some false laziness w/search/cust_bill.html
5577 my $orderby = 'ORDER BY cust_bill._date';
5579 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5581 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5583 my @cust_bill = qsearch( {
5584 #'select' => "cust_bill.*",
5585 'table' => 'cust_bill',
5586 'addl_from' => $addl_from,
5588 'extra_sql' => $extra_sql,
5589 'order_by' => $orderby,
5593 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5595 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5598 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5599 foreach my $cust_bill ( @cust_bill ) {
5600 $cust_bill->$method();
5602 if ( $job ) { #progressbar foo
5604 if ( time - $min_sec > $last ) {
5605 my $error = $job->update_statustext(
5606 int( 100 * $num / scalar(@cust_bill) )
5608 die $error if $error;
5619 =head1 CLASS METHODS
5625 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5630 my ($class, $start, $end) = @_;
5632 $class->paid_sql($start, $end). ' - '.
5633 $class->credited_sql($start, $end);
5638 Returns an SQL fragment to retreive the net amount (charged minus credited).
5643 my ($class, $start, $end) = @_;
5644 'charged - '. $class->credited_sql($start, $end);
5649 Returns an SQL fragment to retreive the amount paid against this invoice.
5654 my ($class, $start, $end) = @_;
5655 $start &&= "AND cust_bill_pay._date <= $start";
5656 $end &&= "AND cust_bill_pay._date > $end";
5657 $start = '' unless defined($start);
5658 $end = '' unless defined($end);
5659 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5660 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5665 Returns an SQL fragment to retreive the amount credited against this invoice.
5670 my ($class, $start, $end) = @_;
5671 $start &&= "AND cust_credit_bill._date <= $start";
5672 $end &&= "AND cust_credit_bill._date > $end";
5673 $start = '' unless defined($start);
5674 $end = '' unless defined($end);
5675 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5676 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5681 Returns an SQL fragment to retrieve the due date of an invoice.
5682 Currently only supported on PostgreSQL.
5687 my $conf = new FS::Conf;
5691 cust_bill.invoice_terms,
5692 cust_main.invoice_terms,
5693 \''.($conf->config('invoice_default_terms') || '').'\'
5694 ), E\'Net (\\\\d+)\'
5696 ) * 86400 + cust_bill._date'
5699 =item search_sql_where HASHREF
5701 Class method which returns an SQL WHERE fragment to search for parameters
5702 specified in HASHREF. Valid parameters are
5708 List reference of start date, end date, as UNIX timestamps.
5718 List reference of charged limits (exclusive).
5722 List reference of charged limits (exclusive).
5726 flag, return open invoices only
5730 flag, return net invoices only
5734 =item newest_percust
5738 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5742 sub search_sql_where {
5743 my($class, $param) = @_;
5745 warn "$me search_sql_where called with params: \n".
5746 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5752 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5753 push @search, "cust_main.agentnum = $1";
5757 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5758 push @search, "cust_main.refnum = $1";
5762 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5763 push @search, "cust_bill.custnum = $1";
5767 if ( $param->{'cust_classnum'} ) {
5768 my $classnums = $param->{'cust_classnum'};
5769 $classnums = [ $classnums ] if !ref($classnums);
5770 $classnums = [ grep /^\d+$/, @$classnums ];
5771 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5776 if ( $param->{_date} ) {
5777 my($beginning, $ending) = @{$param->{_date}};
5779 push @search, "cust_bill._date >= $beginning",
5780 "cust_bill._date < $ending";
5784 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5785 push @search, "cust_bill.invnum >= $1";
5787 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5788 push @search, "cust_bill.invnum <= $1";
5792 if ( $param->{charged} ) {
5793 my @charged = ref($param->{charged})
5794 ? @{ $param->{charged} }
5795 : ($param->{charged});
5797 push @search, map { s/^charged/cust_bill.charged/; $_; }
5801 my $owed_sql = FS::cust_bill->owed_sql;
5804 if ( $param->{owed} ) {
5805 my @owed = ref($param->{owed})
5806 ? @{ $param->{owed} }
5808 push @search, map { s/^owed/$owed_sql/; $_; }
5813 push @search, "0 != $owed_sql"
5814 if $param->{'open'};
5815 push @search, '0 != '. FS::cust_bill->net_sql
5819 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5820 if $param->{'days'};
5823 if ( $param->{'newest_percust'} ) {
5825 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5826 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5828 my @newest_where = map { my $x = $_;
5829 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5832 grep ! /^cust_main./, @search;
5833 my $newest_where = scalar(@newest_where)
5834 ? ' AND '. join(' AND ', @newest_where)
5838 push @search, "cust_bill._date = (
5839 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5840 WHERE newest_cust_bill.custnum = cust_bill.custnum
5846 #promised_date - also has an option to accept nulls
5847 if ( $param->{promised_date} ) {
5848 my($beginning, $ending, $null) = @{$param->{promised_date}};
5850 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5851 "cust_bill.promised_date < $ending )" .
5852 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5855 #agent virtualization
5856 my $curuser = $FS::CurrentUser::CurrentUser;
5857 if ( $curuser->username eq 'fs_queue'
5858 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5860 my $newuser = qsearchs('access_user', {
5861 'username' => $username,
5865 $curuser = $newuser;
5867 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5870 push @search, $curuser->agentnums_sql;
5872 join(' AND ', @search );
5884 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5885 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base