4 use vars qw( @ISA $DEBUG $me
5 $money_char $date_format $rdate_format $date_format_long );
7 use vars qw( $invoice_lines @buf ); #yuck
8 use Fcntl qw(:flock); #for spool_csv
10 use List::Util qw(min max sum);
13 use Text::Template 1.20;
15 use String::ShellQuote;
18 use Storable qw( freeze thaw );
20 use FS::UID qw( datasrc );
21 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
22 use FS::Record qw( qsearch qsearchs dbh );
23 use FS::cust_main_Mixin;
25 use FS::cust_statement;
26 use FS::cust_bill_pkg;
27 use FS::cust_bill_pkg_display;
28 use FS::cust_bill_pkg_detail;
32 use FS::cust_credit_bill;
34 use FS::cust_pay_batch;
35 use FS::cust_bill_event;
38 use FS::cust_bill_pay;
39 use FS::cust_bill_pay_batch;
40 use FS::part_bill_event;
43 use FS::cust_bill_batch;
44 use FS::cust_bill_pay_pkg;
45 use FS::cust_credit_bill_pkg;
46 use FS::discount_plan;
49 @ISA = qw( FS::cust_main_Mixin FS::Record );
52 $me = '[FS::cust_bill]';
54 #ask FS::UID to run this stuff for us later
55 FS::UID->install_callback( sub {
56 my $conf = new FS::Conf; #global
57 $money_char = $conf->config('money_char') || '$';
58 $date_format = $conf->config('date_format') || '%x'; #/YY
59 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
60 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
65 FS::cust_bill - Object methods for cust_bill records
71 $record = new FS::cust_bill \%hash;
72 $record = new FS::cust_bill { 'column' => 'value' };
74 $error = $record->insert;
76 $error = $new_record->replace($old_record);
78 $error = $record->delete;
80 $error = $record->check;
82 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
84 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
86 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
88 @cust_pay_objects = $cust_bill->cust_pay;
90 $tax_amount = $record->tax;
92 @lines = $cust_bill->print_text;
93 @lines = $cust_bill->print_text $time;
97 An FS::cust_bill object represents an invoice; a declaration that a customer
98 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
99 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
100 following fields are currently supported:
106 =item invnum - primary key (assigned automatically for new invoices)
108 =item custnum - customer (see L<FS::cust_main>)
110 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
111 L<Time::Local> and L<Date::Parse> for conversion functions.
113 =item charged - amount of this invoice
115 =item invoice_terms - optional terms override for this specific invoice
119 Customer info at invoice generation time
123 =item previous_balance
125 =item billing_balance
133 =item printed - deprecated
141 =item closed - books closed flag, empty or `Y'
143 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
145 =item agent_invid - legacy invoice number
147 =item promised_date - customer promised payment date, for collection
157 Creates a new invoice. To add the invoice to the database, see L<"insert">.
158 Invoices are normally created by calling the bill method of a customer object
159 (see L<FS::cust_main>).
163 sub table { 'cust_bill'; }
165 sub cust_linked { $_[0]->cust_main_custnum; }
166 sub cust_unlinked_msg {
168 "WARNING: can't find cust_main.custnum ". $self->custnum.
169 ' (cust_bill.invnum '. $self->invnum. ')';
174 Adds this invoice to the database ("Posts" the invoice). If there is an error,
175 returns the error, otherwise returns false.
181 warn "$me insert called\n" if $DEBUG;
183 local $SIG{HUP} = 'IGNORE';
184 local $SIG{INT} = 'IGNORE';
185 local $SIG{QUIT} = 'IGNORE';
186 local $SIG{TERM} = 'IGNORE';
187 local $SIG{TSTP} = 'IGNORE';
188 local $SIG{PIPE} = 'IGNORE';
190 my $oldAutoCommit = $FS::UID::AutoCommit;
191 local $FS::UID::AutoCommit = 0;
194 my $error = $self->SUPER::insert;
196 $dbh->rollback if $oldAutoCommit;
200 if ( $self->get('cust_bill_pkg') ) {
201 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
202 $cust_bill_pkg->invnum($self->invnum);
203 my $error = $cust_bill_pkg->insert;
205 $dbh->rollback if $oldAutoCommit;
206 return "can't create invoice line item: $error";
211 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
218 This method now works but you probably shouldn't use it. Instead, apply a
219 credit against the invoice.
221 Using this method to delete invoices outright is really, really bad. There
222 would be no record you ever posted this invoice, and there are no check to
223 make sure charged = 0 or that there are no associated cust_bill_pkg records.
225 Really, don't use it.
231 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
233 local $SIG{HUP} = 'IGNORE';
234 local $SIG{INT} = 'IGNORE';
235 local $SIG{QUIT} = 'IGNORE';
236 local $SIG{TERM} = 'IGNORE';
237 local $SIG{TSTP} = 'IGNORE';
238 local $SIG{PIPE} = 'IGNORE';
240 my $oldAutoCommit = $FS::UID::AutoCommit;
241 local $FS::UID::AutoCommit = 0;
244 foreach my $table (qw(
256 foreach my $linked ( $self->$table() ) {
257 my $error = $linked->delete;
259 $dbh->rollback if $oldAutoCommit;
266 my $error = $self->SUPER::delete(@_);
268 $dbh->rollback if $oldAutoCommit;
272 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
278 =item replace [ OLD_RECORD ]
280 You can, but probably shouldn't modify invoices...
282 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
283 supplied, replaces this record. If there is an error, returns the error,
284 otherwise returns false.
288 #replace can be inherited from Record.pm
290 # replace_check is now the preferred way to #implement replace data checks
291 # (so $object->replace() works without an argument)
294 my( $new, $old ) = ( shift, shift );
295 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
296 #return "Can't change _date!" unless $old->_date eq $new->_date;
297 return "Can't change _date" unless $old->_date == $new->_date;
298 return "Can't change charged" unless $old->charged == $new->charged
299 || $old->charged == 0
300 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
306 =item add_cc_surcharge
312 sub add_cc_surcharge {
313 my ($self, $pkgnum, $amount) = (shift, shift, shift);
316 my $cust_bill_pkg = new FS::cust_bill_pkg({
317 'invnum' => $self->invnum,
321 $error = $cust_bill_pkg->insert;
322 return $error if $error;
324 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
325 $self->charged($self->charged+$amount);
326 $error = $self->replace;
327 return $error if $error;
329 $self->apply_payments_and_credits;
335 Checks all fields to make sure this is a valid invoice. If there is an error,
336 returns the error, otherwise returns false. Called by the insert and replace
345 $self->ut_numbern('invnum')
346 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
347 || $self->ut_numbern('_date')
348 || $self->ut_money('charged')
349 || $self->ut_numbern('printed')
350 || $self->ut_enum('closed', [ '', 'Y' ])
351 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
352 || $self->ut_numbern('agent_invid') #varchar?
354 return $error if $error;
356 $self->_date(time) unless $self->_date;
358 $self->printed(0) if $self->printed eq '';
365 Returns the displayed invoice number for this invoice: agent_invid if
366 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
372 my $conf = $self->conf;
373 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
374 return $self->agent_invid;
376 return $self->invnum;
382 Returns a list consisting of the total previous balance for this customer,
383 followed by the previous outstanding invoices (as FS::cust_bill objects also).
390 my @cust_bill = sort { $a->_date <=> $b->_date }
391 grep { $_->owed != 0 }
392 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
393 #'_date' => { op=>'<', value=>$self->_date },
394 'invnum' => { op=>'<', value=>$self->invnum },
397 foreach ( @cust_bill ) { $total += $_->owed; }
401 =item enable_previous
403 Whether to show the 'Previous Charges' section when printing this invoice.
404 The negation of the 'disable_previous_balance' config setting.
408 sub enable_previous {
410 my $agentnum = $self->cust_main->agentnum;
411 !$self->conf->exists('disable_previous_balance', $agentnum);
416 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
423 { 'table' => 'cust_bill_pkg',
424 'hashref' => { 'invnum' => $self->invnum },
425 'order_by' => 'ORDER BY billpkgnum',
430 =item cust_bill_pkg_pkgnum PKGNUM
432 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
437 sub cust_bill_pkg_pkgnum {
438 my( $self, $pkgnum ) = @_;
440 { 'table' => 'cust_bill_pkg',
441 'hashref' => { 'invnum' => $self->invnum,
444 'order_by' => 'ORDER BY billpkgnum',
451 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
458 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
459 $self->cust_bill_pkg;
461 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
466 Returns true if any of the packages (or their definitions) corresponding to the
467 line items for this invoice have the no_auto flag set.
473 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
476 =item open_cust_bill_pkg
478 Returns the open line items for this invoice.
480 Note that cust_bill_pkg with both setup and recur fees are returned as two
481 separate line items, each with only one fee.
485 # modeled after cust_main::open_cust_bill
486 sub open_cust_bill_pkg {
489 # grep { $_->owed > 0 } $self->cust_bill_pkg
491 my %other = ( 'recur' => 'setup',
492 'setup' => 'recur', );
494 foreach my $field ( qw( recur setup )) {
495 push @open, map { $_->set( $other{$field}, 0 ); $_; }
496 grep { $_->owed($field) > 0 }
497 $self->cust_bill_pkg;
503 =item cust_bill_event
505 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
509 sub cust_bill_event {
511 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
514 =item num_cust_bill_event
516 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
520 sub num_cust_bill_event {
523 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
524 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
525 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
526 $sth->fetchrow_arrayref->[0];
531 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
535 #false laziness w/cust_pkg.pm
539 'table' => 'cust_event',
540 'addl_from' => 'JOIN part_event USING ( eventpart )',
541 'hashref' => { 'tablenum' => $self->invnum },
542 'extra_sql' => " AND eventtable = 'cust_bill' ",
548 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
552 #false laziness w/cust_pkg.pm
556 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
557 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
558 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
559 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
560 $sth->fetchrow_arrayref->[0];
565 Returns the customer (see L<FS::cust_main>) for this invoice.
571 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
574 =item cust_suspend_if_balance_over AMOUNT
576 Suspends the customer associated with this invoice if the total amount owed on
577 this invoice and all older invoices is greater than the specified amount.
579 Returns a list: an empty list on success or a list of errors.
583 sub cust_suspend_if_balance_over {
584 my( $self, $amount ) = ( shift, shift );
585 my $cust_main = $self->cust_main;
586 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
589 $cust_main->suspend(@_);
595 Depreciated. See the cust_credited method.
597 #Returns a list consisting of the total previous credited (see
598 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
599 #outstanding credits (FS::cust_credit objects).
605 croak "FS::cust_bill->cust_credit depreciated; see ".
606 "FS::cust_bill->cust_credit_bill";
609 #my @cust_credit = sort { $a->_date <=> $b->_date }
610 # grep { $_->credited != 0 && $_->_date < $self->_date }
611 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
613 #foreach (@cust_credit) { $total += $_->credited; }
614 #$total, @cust_credit;
619 Depreciated. See the cust_bill_pay method.
621 #Returns all payments (see L<FS::cust_pay>) for this invoice.
627 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
629 #sort { $a->_date <=> $b->_date }
630 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
636 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
639 sub cust_bill_pay_batch {
641 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
646 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
652 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
653 sort { $a->_date <=> $b->_date }
654 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
659 =item cust_credit_bill
661 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
667 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
668 sort { $a->_date <=> $b->_date }
669 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
673 sub cust_credit_bill {
674 shift->cust_credited(@_);
677 #=item cust_bill_pay_pkgnum PKGNUM
679 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
680 #with matching pkgnum.
684 #sub cust_bill_pay_pkgnum {
685 # my( $self, $pkgnum ) = @_;
686 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
687 # sort { $a->_date <=> $b->_date }
688 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
689 # 'pkgnum' => $pkgnum,
694 =item cust_bill_pay_pkg PKGNUM
696 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
697 applied against the matching pkgnum.
701 sub cust_bill_pay_pkg {
702 my( $self, $pkgnum ) = @_;
705 'select' => 'cust_bill_pay_pkg.*',
706 'table' => 'cust_bill_pay_pkg',
707 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
708 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
709 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
710 " AND cust_bill_pkg.pkgnum = $pkgnum",
715 #=item cust_credited_pkgnum PKGNUM
717 #=item cust_credit_bill_pkgnum PKGNUM
719 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
720 #with matching pkgnum.
724 #sub cust_credited_pkgnum {
725 # my( $self, $pkgnum ) = @_;
726 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
727 # sort { $a->_date <=> $b->_date }
728 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
729 # 'pkgnum' => $pkgnum,
734 #sub cust_credit_bill_pkgnum {
735 # shift->cust_credited_pkgnum(@_);
738 =item cust_credit_bill_pkg PKGNUM
740 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
741 applied against the matching pkgnum.
745 sub cust_credit_bill_pkg {
746 my( $self, $pkgnum ) = @_;
749 'select' => 'cust_credit_bill_pkg.*',
750 'table' => 'cust_credit_bill_pkg',
751 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
752 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
753 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
754 " AND cust_bill_pkg.pkgnum = $pkgnum",
759 =item cust_bill_batch
761 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
765 sub cust_bill_batch {
767 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
772 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
773 hash keyed by term length.
779 FS::discount_plan->all($self);
784 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
791 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
793 foreach (@taxlines) { $total += $_->setup; }
799 Returns the amount owed (still outstanding) on this invoice, which is charged
800 minus all payment applications (see L<FS::cust_bill_pay>) and credit
801 applications (see L<FS::cust_credit_bill>).
807 my $balance = $self->charged;
808 $balance -= $_->amount foreach ( $self->cust_bill_pay );
809 $balance -= $_->amount foreach ( $self->cust_credited );
810 $balance = sprintf( "%.2f", $balance);
811 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
816 my( $self, $pkgnum ) = @_;
818 #my $balance = $self->charged;
820 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
822 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
823 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
825 $balance = sprintf( "%.2f", $balance);
826 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
832 Returns true if this invoice should be hidden. See the
833 selfservice-hide_invoices-taxclass configuraiton setting.
839 my $conf = $self->conf;
840 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
842 my @cust_bill_pkg = $self->cust_bill_pkg;
843 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
844 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
847 =item apply_payments_and_credits [ OPTION => VALUE ... ]
849 Applies unapplied payments and credits to this invoice.
851 A hash of optional arguments may be passed. Currently "manual" is supported.
852 If true, a payment receipt is sent instead of a statement when
853 'payment_receipt_email' configuration option is set.
855 If there is an error, returns the error, otherwise returns false.
859 sub apply_payments_and_credits {
860 my( $self, %options ) = @_;
861 my $conf = $self->conf;
863 local $SIG{HUP} = 'IGNORE';
864 local $SIG{INT} = 'IGNORE';
865 local $SIG{QUIT} = 'IGNORE';
866 local $SIG{TERM} = 'IGNORE';
867 local $SIG{TSTP} = 'IGNORE';
868 local $SIG{PIPE} = 'IGNORE';
870 my $oldAutoCommit = $FS::UID::AutoCommit;
871 local $FS::UID::AutoCommit = 0;
874 $self->select_for_update; #mutex
876 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
877 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
879 if ( $conf->exists('pkg-balances') ) {
880 # limit @payments & @credits to those w/ a pkgnum grepped from $self
881 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
882 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
883 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
886 while ( $self->owed > 0 and ( @payments || @credits ) ) {
889 if ( @payments && @credits ) {
891 #decide which goes first by weight of top (unapplied) line item
893 my @open_lineitems = $self->open_cust_bill_pkg;
896 max( map { $_->part_pkg->pay_weight || 0 }
901 my $max_credit_weight =
902 max( map { $_->part_pkg->credit_weight || 0 }
908 #if both are the same... payments first? it has to be something
909 if ( $max_pay_weight >= $max_credit_weight ) {
915 } elsif ( @payments ) {
917 } elsif ( @credits ) {
920 die "guru meditation #12 and 35";
924 if ( $app eq 'pay' ) {
926 my $payment = shift @payments;
927 $unapp_amount = $payment->unapplied;
928 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
929 $app->pkgnum( $payment->pkgnum )
930 if $conf->exists('pkg-balances') && $payment->pkgnum;
932 } elsif ( $app eq 'credit' ) {
934 my $credit = shift @credits;
935 $unapp_amount = $credit->credited;
936 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
937 $app->pkgnum( $credit->pkgnum )
938 if $conf->exists('pkg-balances') && $credit->pkgnum;
941 die "guru meditation #12 and 35";
945 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
946 warn "owed_pkgnum ". $app->pkgnum;
947 $owed = $self->owed_pkgnum($app->pkgnum);
951 next unless $owed > 0;
953 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
954 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
956 $app->invnum( $self->invnum );
958 my $error = $app->insert(%options);
960 $dbh->rollback if $oldAutoCommit;
961 return "Error inserting ". $app->table. " record: $error";
963 die $error if $error;
967 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
972 =item generate_email OPTION => VALUE ...
980 sender address, required
984 alternate template name, optional
988 text attachment arrayref, optional
992 email subject, optional
996 notice name instead of "Invoice", optional
1000 Returns an argument list to be passed to L<FS::Misc::send_email>.
1006 sub generate_email {
1010 my $conf = $self->conf;
1012 my $me = '[FS::cust_bill::generate_email]';
1015 'from' => $args{'from'},
1016 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
1020 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
1021 'template' => $args{'template'},
1022 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
1023 'no_coupon' => $args{'no_coupon'},
1026 my $cust_main = $self->cust_main;
1028 if (ref($args{'to'}) eq 'ARRAY') {
1029 $return{'to'} = $args{'to'};
1031 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1032 $cust_main->invoicing_list
1036 if ( $conf->exists('invoice_html') ) {
1038 warn "$me creating HTML/text multipart message"
1041 $return{'nobody'} = 1;
1043 my $alternative = build MIME::Entity
1044 'Type' => 'multipart/alternative',
1045 #'Encoding' => '7bit',
1046 'Disposition' => 'inline'
1050 if ( $conf->exists('invoice_email_pdf')
1051 and scalar($conf->config('invoice_email_pdf_note')) ) {
1053 warn "$me using 'invoice_email_pdf_note' in multipart message"
1055 $data = [ map { $_ . "\n" }
1056 $conf->config('invoice_email_pdf_note')
1061 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1063 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1064 $data = $args{'print_text'};
1066 $data = [ $self->print_text(\%opt) ];
1071 $alternative->attach(
1072 'Type' => 'text/plain',
1073 'Encoding' => 'quoted-printable',
1074 #'Encoding' => '7bit',
1076 'Disposition' => 'inline',
1083 if ( $conf->exists('invoice_email_pdf')
1084 and scalar($conf->config('invoice_email_pdf_note')) ) {
1086 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1090 $args{'from'} =~ /\@([\w\.\-]+)/;
1091 my $from = $1 || 'example.com';
1092 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1095 my $agentnum = $cust_main->agentnum;
1096 if ( defined($args{'template'}) && length($args{'template'})
1097 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1100 $logo = 'logo_'. $args{'template'}. '.png';
1104 my $image_data = $conf->config_binary( $logo, $agentnum);
1106 $image = build MIME::Entity
1107 'Type' => 'image/png',
1108 'Encoding' => 'base64',
1109 'Data' => $image_data,
1110 'Filename' => 'logo.png',
1111 'Content-ID' => "<$content_id>",
1114 if ($conf->exists('invoice-barcode')) {
1115 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1116 $barcode = build MIME::Entity
1117 'Type' => 'image/png',
1118 'Encoding' => 'base64',
1119 'Data' => $self->invoice_barcode(0),
1120 'Filename' => 'barcode.png',
1121 'Content-ID' => "<$barcode_content_id>",
1123 $opt{'barcode_cid'} = $barcode_content_id;
1126 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1129 $alternative->attach(
1130 'Type' => 'text/html',
1131 'Encoding' => 'quoted-printable',
1132 'Data' => [ '<html>',
1135 ' '. encode_entities($return{'subject'}),
1138 ' <body bgcolor="#e8e8e8">',
1143 'Disposition' => 'inline',
1144 #'Filename' => 'invoice.pdf',
1148 my @otherparts = ();
1149 if ( $cust_main->email_csv_cdr ) {
1151 push @otherparts, build MIME::Entity
1152 'Type' => 'text/csv',
1153 'Encoding' => '7bit',
1154 'Data' => [ map { "$_\n" }
1155 $self->call_details('prepend_billed_number' => 1)
1157 'Disposition' => 'attachment',
1158 'Filename' => 'usage-'. $self->invnum. '.csv',
1163 if ( $conf->exists('invoice_email_pdf') ) {
1168 # multipart/alternative
1174 my $related = build MIME::Entity 'Type' => 'multipart/related',
1175 'Encoding' => '7bit';
1177 #false laziness w/Misc::send_email
1178 $related->head->replace('Content-type',
1179 $related->mime_type.
1180 '; boundary="'. $related->head->multipart_boundary. '"'.
1181 '; type=multipart/alternative'
1184 $related->add_part($alternative);
1186 $related->add_part($image) if $image;
1188 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1190 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1194 #no other attachment:
1196 # multipart/alternative
1201 $return{'content-type'} = 'multipart/related';
1202 if ($conf->exists('invoice-barcode') && $barcode) {
1203 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1205 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1207 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1208 #$return{'disposition'} = 'inline';
1214 if ( $conf->exists('invoice_email_pdf') ) {
1215 warn "$me creating PDF attachment"
1218 #mime parts arguments a la MIME::Entity->build().
1219 $return{'mimeparts'} = [
1220 { $self->mimebuild_pdf(\%opt) }
1224 if ( $conf->exists('invoice_email_pdf')
1225 and scalar($conf->config('invoice_email_pdf_note')) ) {
1227 warn "$me using 'invoice_email_pdf_note'"
1229 $return{'body'} = [ map { $_ . "\n" }
1230 $conf->config('invoice_email_pdf_note')
1235 warn "$me not using 'invoice_email_pdf_note'"
1237 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1238 $return{'body'} = $args{'print_text'};
1240 $return{'body'} = [ $self->print_text(\%opt) ];
1253 Returns a list suitable for passing to MIME::Entity->build(), representing
1254 this invoice as PDF attachment.
1261 'Type' => 'application/pdf',
1262 'Encoding' => 'base64',
1263 'Data' => [ $self->print_pdf(@_) ],
1264 'Disposition' => 'attachment',
1265 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1269 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1271 Sends this invoice to the destinations configured for this customer: sends
1272 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1274 Options can be passed as a hashref (recommended) or as a list of up to
1275 four values for templatename, agentnum, invoice_from and amount.
1277 I<template>, if specified, is the name of a suffix for alternate invoices.
1279 I<agentnum>, if specified, means that this invoice will only be sent for customers
1280 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1281 single agent) or an arrayref of agentnums.
1283 I<invoice_from>, if specified, overrides the default email invoice From: address.
1285 I<amount>, if specified, only sends the invoice if the total amount owed on this
1286 invoice and all older invoices is greater than the specified amount.
1288 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1292 sub queueable_send {
1295 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1296 or die "invalid invoice number: " . $opt{invnum};
1298 my @args = ( $opt{template}, $opt{agentnum} );
1299 push @args, $opt{invoice_from}
1300 if exists($opt{invoice_from}) && $opt{invoice_from};
1302 my $error = $self->send( @args );
1303 die $error if $error;
1309 my $conf = $self->conf;
1311 my( $template, $invoice_from, $notice_name );
1313 my $balance_over = 0;
1317 $template = $opt->{'template'} || '';
1318 if ( $agentnums = $opt->{'agentnum'} ) {
1319 $agentnums = [ $agentnums ] unless ref($agentnums);
1321 $invoice_from = $opt->{'invoice_from'};
1322 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1323 $notice_name = $opt->{'notice_name'};
1325 $template = scalar(@_) ? shift : '';
1326 if ( scalar(@_) && $_[0] ) {
1327 $agentnums = ref($_[0]) ? shift : [ shift ];
1329 $invoice_from = shift if scalar(@_);
1330 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1333 my $cust_main = $self->cust_main;
1335 return 'N/A' unless ! $agentnums
1336 or grep { $_ == $cust_main->agentnum } @$agentnums;
1339 unless $cust_main->total_owed_date($self->_date) > $balance_over;
1341 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1342 $conf->config('invoice_from', $cust_main->agentnum );
1345 'template' => $template,
1346 'invoice_from' => $invoice_from,
1347 'notice_name' => ( $notice_name || 'Invoice' ),
1350 my @invoicing_list = $cust_main->invoicing_list;
1352 #$self->email_invoice(\%opt)
1354 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1355 && ! $self->invoice_noemail;
1357 #$self->print_invoice(\%opt)
1359 if grep { $_ eq 'POST' } @invoicing_list; #postal
1361 $self->fax_invoice(\%opt)
1362 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1368 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1370 Emails this invoice.
1372 Options can be passed as a hashref (recommended) or as a list of up to
1373 two values for templatename and invoice_from.
1375 I<template>, if specified, is the name of a suffix for alternate invoices.
1377 I<invoice_from>, if specified, overrides the default email invoice From: address.
1379 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1383 sub queueable_email {
1386 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1387 or die "invalid invoice number: " . $opt{invnum};
1389 my %args = ( 'template' => $opt{template} );
1390 $args{$_} = $opt{$_}
1391 foreach grep { exists($opt{$_}) && $opt{$_} }
1392 qw( invoice_from notice_name no_coupon );
1394 my $error = $self->email( \%args );
1395 die $error if $error;
1399 #sub email_invoice {
1402 return if $self->hide;
1403 my $conf = $self->conf;
1405 my( $template, $invoice_from, $notice_name, $no_coupon );
1408 $template = $opt->{'template'} || '';
1409 $invoice_from = $opt->{'invoice_from'};
1410 $notice_name = $opt->{'notice_name'} || 'Invoice';
1411 $no_coupon = $opt->{'no_coupon'} || 0;
1413 $template = scalar(@_) ? shift : '';
1414 $invoice_from = shift if scalar(@_);
1415 $notice_name = 'Invoice';
1419 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1420 $conf->config('invoice_from', $self->cust_main->agentnum );
1422 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1423 $self->cust_main->invoicing_list;
1425 if ( ! @invoicing_list ) { #no recipients
1426 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1427 die 'No recipients for customer #'. $self->custnum;
1429 #default: better to notify this person than silence
1430 @invoicing_list = ($invoice_from);
1434 my $subject = $self->email_subject($template);
1436 my $error = send_email(
1437 $self->generate_email(
1438 'from' => $invoice_from,
1439 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1440 'subject' => $subject,
1441 'template' => $template,
1442 'notice_name' => $notice_name,
1443 'no_coupon' => $no_coupon,
1446 die "can't email invoice: $error\n" if $error;
1447 #die "$error\n" if $error;
1453 my $conf = $self->conf;
1455 #my $template = scalar(@_) ? shift : '';
1458 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1461 my $cust_main = $self->cust_main;
1462 my $name = $cust_main->name;
1463 my $name_short = $cust_main->name_short;
1464 my $invoice_number = $self->invnum;
1465 my $invoice_date = $self->_date_pretty;
1467 eval qq("$subject");
1470 =item lpr_data HASHREF | [ TEMPLATE ]
1472 Returns the postscript or plaintext for this invoice as an arrayref.
1474 Options can be passed as a hashref (recommended) or as a single optional value
1477 I<template>, if specified, is the name of a suffix for alternate invoices.
1479 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1485 my $conf = $self->conf;
1486 my( $template, $notice_name );
1489 $template = $opt->{'template'} || '';
1490 $notice_name = $opt->{'notice_name'} || 'Invoice';
1492 $template = scalar(@_) ? shift : '';
1493 $notice_name = 'Invoice';
1497 'template' => $template,
1498 'notice_name' => $notice_name,
1501 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1502 [ $self->$method( \%opt ) ];
1505 =item print HASHREF | [ TEMPLATE ]
1507 Prints this invoice.
1509 Options can be passed as a hashref (recommended) or as a single optional
1512 I<template>, if specified, is the name of a suffix for alternate invoices.
1514 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1518 #sub print_invoice {
1521 return if $self->hide;
1522 my $conf = $self->conf;
1524 my( $template, $notice_name );
1527 $template = $opt->{'template'} || '';
1528 $notice_name = $opt->{'notice_name'} || 'Invoice';
1530 $template = scalar(@_) ? shift : '';
1531 $notice_name = 'Invoice';
1535 'template' => $template,
1536 'notice_name' => $notice_name,
1539 if($conf->exists('invoice_print_pdf')) {
1540 # Add the invoice to the current batch.
1541 $self->batch_invoice(\%opt);
1544 do_print $self->lpr_data(\%opt);
1548 =item fax_invoice HASHREF | [ TEMPLATE ]
1552 Options can be passed as a hashref (recommended) or as a single optional
1555 I<template>, if specified, is the name of a suffix for alternate invoices.
1557 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1563 return if $self->hide;
1564 my $conf = $self->conf;
1566 my( $template, $notice_name );
1569 $template = $opt->{'template'} || '';
1570 $notice_name = $opt->{'notice_name'} || 'Invoice';
1572 $template = scalar(@_) ? shift : '';
1573 $notice_name = 'Invoice';
1576 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1577 unless $conf->exists('invoice_latex');
1579 my $dialstring = $self->cust_main->getfield('fax');
1583 'template' => $template,
1584 'notice_name' => $notice_name,
1587 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1588 'dialstring' => $dialstring,
1590 die $error if $error;
1594 =item batch_invoice [ HASHREF ]
1596 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1597 isn't an open batch, one will be created.
1602 my ($self, $opt) = @_;
1603 my $bill_batch = $self->get_open_bill_batch;
1604 my $cust_bill_batch = FS::cust_bill_batch->new({
1605 batchnum => $bill_batch->batchnum,
1606 invnum => $self->invnum,
1608 return $cust_bill_batch->insert($opt);
1611 =item get_open_batch
1613 Returns the currently open batch as an FS::bill_batch object, creating a new
1614 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1619 sub get_open_bill_batch {
1621 my $conf = $self->conf;
1622 my $hashref = { status => 'O' };
1623 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1624 ? $self->cust_main->agentnum
1626 my $batch = qsearchs('bill_batch', $hashref);
1627 return $batch if $batch;
1628 $batch = FS::bill_batch->new($hashref);
1629 my $error = $batch->insert;
1630 die $error if $error;
1634 =item ftp_invoice [ TEMPLATENAME ]
1636 Sends this invoice data via FTP.
1638 TEMPLATENAME is unused?
1644 my $conf = $self->conf;
1645 my $template = scalar(@_) ? shift : '';
1648 'protocol' => 'ftp',
1649 'server' => $conf->config('cust_bill-ftpserver'),
1650 'username' => $conf->config('cust_bill-ftpusername'),
1651 'password' => $conf->config('cust_bill-ftppassword'),
1652 'dir' => $conf->config('cust_bill-ftpdir'),
1653 'format' => $conf->config('cust_bill-ftpformat'),
1657 =item spool_invoice [ TEMPLATENAME ]
1659 Spools this invoice data (see L<FS::spool_csv>)
1661 TEMPLATENAME is unused?
1667 my $conf = $self->conf;
1668 my $template = scalar(@_) ? shift : '';
1671 'format' => $conf->config('cust_bill-spoolformat'),
1672 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1676 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1678 Like B<send>, but only sends the invoice if it is the newest open invoice for
1683 sub send_if_newest {
1688 grep { $_->owed > 0 }
1689 qsearch('cust_bill', {
1690 'custnum' => $self->custnum,
1691 #'_date' => { op=>'>', value=>$self->_date },
1692 'invnum' => { op=>'>', value=>$self->invnum },
1699 =item send_csv OPTION => VALUE, ...
1701 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1705 protocol - currently only "ftp"
1711 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1712 and YYMMDDHHMMSS is a timestamp.
1714 See L</print_csv> for a description of the output format.
1719 my($self, %opt) = @_;
1723 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1724 mkdir $spooldir, 0700 unless -d $spooldir;
1726 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1727 my $file = "$spooldir/$tracctnum.csv";
1729 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1731 open(CSV, ">$file") or die "can't open $file: $!";
1739 if ( $opt{protocol} eq 'ftp' ) {
1740 eval "use Net::FTP;";
1742 $net = Net::FTP->new($opt{server}) or die @$;
1744 die "unknown protocol: $opt{protocol}";
1747 $net->login( $opt{username}, $opt{password} )
1748 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1750 $net->binary or die "can't set binary mode";
1752 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1754 $net->put($file) or die "can't put $file: $!";
1764 Spools CSV invoice data.
1770 =item format - 'default' or 'billco'
1772 =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>).
1774 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1776 =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.
1783 my($self, %opt) = @_;
1785 my $cust_main = $self->cust_main;
1787 if ( $opt{'dest'} ) {
1788 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1789 $cust_main->invoicing_list;
1790 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1791 || ! keys %invoicing_list;
1794 if ( $opt{'balanceover'} ) {
1796 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1799 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1800 mkdir $spooldir, 0700 unless -d $spooldir;
1802 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1806 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1807 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1810 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1812 open(CSV, ">>$file") or die "can't open $file: $!";
1813 flock(CSV, LOCK_EX);
1818 if ( lc($opt{'format'}) eq 'billco' ) {
1820 flock(CSV, LOCK_UN);
1825 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1828 open(CSV,">>$file") or die "can't open $file: $!";
1829 flock(CSV, LOCK_EX);
1835 flock(CSV, LOCK_UN);
1842 =item print_csv OPTION => VALUE, ...
1844 Returns CSV data for this invoice.
1848 format - 'default' or 'billco'
1850 Returns a list consisting of two scalars. The first is a single line of CSV
1851 header information for this invoice. The second is one or more lines of CSV
1852 detail information for this invoice.
1854 If I<format> is not specified or "default", the fields of the CSV file are as
1857 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1861 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1863 B<record_type> is C<cust_bill> for the initial header line only. The
1864 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1865 fields are filled in.
1867 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1868 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1871 =item invnum - invoice number
1873 =item custnum - customer number
1875 =item _date - invoice date
1877 =item charged - total invoice amount
1879 =item first - customer first name
1881 =item last - customer first name
1883 =item company - company name
1885 =item address1 - address line 1
1887 =item address2 - address line 1
1897 =item pkg - line item description
1899 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1901 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1903 =item sdate - start date for recurring fee
1905 =item edate - end date for recurring fee
1909 If I<format> is "billco", the fields of the header CSV file are as follows:
1911 +-------------------------------------------------------------------+
1912 | FORMAT HEADER FILE |
1913 |-------------------------------------------------------------------|
1914 | Field | Description | Name | Type | Width |
1915 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1916 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1917 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1918 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1919 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1920 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1921 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1922 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1923 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1924 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1925 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1926 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1927 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1928 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1929 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1930 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1931 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1932 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1933 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1934 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1935 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1936 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1937 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1938 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1939 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1940 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1941 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1942 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1943 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1944 +-------+-------------------------------+------------+------+-------+
1946 If I<format> is "billco", the fields of the detail CSV file are as follows:
1948 FORMAT FOR DETAIL FILE
1950 Field | Description | Name | Type | Width
1951 1 | N/A-Leave Empty | RC | CHAR | 2
1952 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1953 3 | Account Number | TRACCTNUM | CHAR | 15
1954 4 | Invoice Number | TRINVOICE | CHAR | 15
1955 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1956 6 | Transaction Detail | DETAILS | CHAR | 100
1957 7 | Amount | AMT | NUM* | 9
1958 8 | Line Format Control** | LNCTRL | CHAR | 2
1959 9 | Grouping Code | GROUP | CHAR | 2
1960 10 | User Defined | ACCT CODE | CHAR | 15
1965 my($self, %opt) = @_;
1967 eval "use Text::CSV_XS";
1970 my $cust_main = $self->cust_main;
1972 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1974 if ( lc($opt{'format'}) eq 'billco' ) {
1977 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1979 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1981 my( $previous_balance, @unused ) = $self->previous; #previous balance
1983 my $pmt_cr_applied = 0;
1984 $pmt_cr_applied += $_->{'amount'}
1985 foreach ( $self->_items_payments, $self->_items_credits ) ;
1987 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1990 '', # 1 | N/A-Leave Empty CHAR 2
1991 '', # 2 | N/A-Leave Empty CHAR 15
1992 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1993 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1994 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1995 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1996 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1997 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1998 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1999 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2000 '', # 10 | Ancillary Billing Information CHAR 30
2001 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2002 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2005 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2008 $duedate, # 14 | Bill Due Date CHAR 10
2010 $previous_balance, # 15 | Previous Balance NUM* 9
2011 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2012 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2013 $totaldue, # 18 | Total Amt Due NUM* 9
2014 $totaldue, # 19 | Total Amt Due NUM* 9
2015 '', # 20 | 30 Day Aging NUM* 9
2016 '', # 21 | 60 Day Aging NUM* 9
2017 '', # 22 | 90 Day Aging NUM* 9
2018 'N', # 23 | Y/N CHAR 1
2019 '', # 24 | Remittance automation CHAR 100
2020 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2021 $self->custnum, # 26 | Customer Reference Number CHAR 15
2022 '0', # 27 | Federal Tax*** NUM* 9
2023 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2024 '0', # 29 | Other Taxes & Fees*** NUM* 9
2027 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2029 my ($previous_balance) = $self->previous;
2030 $previous_balance = sprintf('%.2f', $previous_balance);
2031 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2033 ($_->{pkgnum} || ''),
2036 } $self->_items_pkg;
2039 $cust_main->agentnum,
2040 $cust_main->agent->agent,
2044 $cust_main->address1,
2045 $cust_main->address2,
2051 time2str("%x", $self->_date),
2056 $self->due_date2str("%x"),
2067 time2str("%x", $self->_date),
2068 sprintf("%.2f", $self->charged),
2069 ( map { $cust_main->getfield($_) }
2070 qw( first last company address1 address2 city state zip country ) ),
2072 ) or die "can't create csv";
2075 my $header = $csv->string. "\n";
2078 if ( lc($opt{'format'}) eq 'billco' ) {
2081 foreach my $item ( $self->_items_pkg ) {
2084 '', # 1 | N/A-Leave Empty CHAR 2
2085 '', # 2 | N/A-Leave Empty CHAR 15
2086 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2087 $self->invnum, # 4 | Invoice Number CHAR 15
2088 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2089 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2090 $item->{'amount'}, # 7 | Amount NUM* 9
2091 '', # 8 | Line Format Control** CHAR 2
2092 '', # 9 | Grouping Code CHAR 2
2093 '', # 10 | User Defined CHAR 15
2096 $detail .= $csv->string. "\n";
2100 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2106 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2108 my($pkg, $setup, $recur, $sdate, $edate);
2109 if ( $cust_bill_pkg->pkgnum ) {
2111 ($pkg, $setup, $recur, $sdate, $edate) = (
2112 $cust_bill_pkg->part_pkg->pkg,
2113 ( $cust_bill_pkg->setup != 0
2114 ? sprintf("%.2f", $cust_bill_pkg->setup )
2116 ( $cust_bill_pkg->recur != 0
2117 ? sprintf("%.2f", $cust_bill_pkg->recur )
2119 ( $cust_bill_pkg->sdate
2120 ? time2str("%x", $cust_bill_pkg->sdate)
2122 ($cust_bill_pkg->edate
2123 ?time2str("%x", $cust_bill_pkg->edate)
2127 } else { #pkgnum tax
2128 next unless $cust_bill_pkg->setup != 0;
2129 $pkg = $cust_bill_pkg->desc;
2130 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2131 ( $sdate, $edate ) = ( '', '' );
2137 ( map { '' } (1..11) ),
2138 ($pkg, $setup, $recur, $sdate, $edate)
2139 ) or die "can't create csv";
2141 $detail .= $csv->string. "\n";
2147 ( $header, $detail );
2153 Pays this invoice with a compliemntary payment. If there is an error,
2154 returns the error, otherwise returns false.
2160 my $cust_pay = new FS::cust_pay ( {
2161 'invnum' => $self->invnum,
2162 'paid' => $self->owed,
2165 'payinfo' => $self->cust_main->payinfo,
2173 Attempts to pay this invoice with a credit card payment via a
2174 Business::OnlinePayment realtime gateway. See
2175 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2176 for supported processors.
2182 $self->realtime_bop( 'CC', @_ );
2187 Attempts to pay this invoice with an electronic check (ACH) payment via a
2188 Business::OnlinePayment realtime gateway. See
2189 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2190 for supported processors.
2196 $self->realtime_bop( 'ECHECK', @_ );
2201 Attempts to pay this invoice with phone bill (LEC) payment via a
2202 Business::OnlinePayment realtime gateway. See
2203 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2204 for supported processors.
2210 $self->realtime_bop( 'LEC', @_ );
2214 my( $self, $method ) = (shift,shift);
2215 my $conf = $self->conf;
2218 my $cust_main = $self->cust_main;
2219 my $balance = $cust_main->balance;
2220 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2221 $amount = sprintf("%.2f", $amount);
2222 return "not run (balance $balance)" unless $amount > 0;
2224 my $description = 'Internet Services';
2225 if ( $conf->exists('business-onlinepayment-description') ) {
2226 my $dtempl = $conf->config('business-onlinepayment-description');
2228 my $agent_obj = $cust_main->agent
2229 or die "can't retreive agent for $cust_main (agentnum ".
2230 $cust_main->agentnum. ")";
2231 my $agent = $agent_obj->agent;
2232 my $pkgs = join(', ',
2233 map { $_->part_pkg->pkg }
2234 grep { $_->pkgnum } $self->cust_bill_pkg
2236 $description = eval qq("$dtempl");
2239 $cust_main->realtime_bop($method, $amount,
2240 'description' => $description,
2241 'invnum' => $self->invnum,
2242 #this didn't do what we want, it just calls apply_payments_and_credits
2244 'apply_to_invoice' => 1,
2247 #this changes application behavior: auto payments
2248 #triggered against a specific invoice are now applied
2249 #to that invoice instead of oldest open.
2255 =item batch_card OPTION => VALUE...
2257 Adds a payment for this invoice to the pending credit card batch (see
2258 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2259 runs the payment using a realtime gateway.
2264 my ($self, %options) = @_;
2265 my $cust_main = $self->cust_main;
2267 $options{invnum} = $self->invnum;
2269 $cust_main->batch_card(%options);
2272 sub _agent_template {
2274 $self->cust_main->agent_template;
2277 sub _agent_invoice_from {
2279 $self->cust_main->agent_invoice_from;
2282 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2284 Returns an text invoice, as a list of lines.
2286 Options can be passed as a hashref (recommended) or as a list of time, template
2287 and then any key/value pairs for any other options.
2289 I<time>, if specified, is used to control the printing of overdue messages. The
2290 default is now. It isn't the date of the invoice; that's the `_date' field.
2291 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2292 L<Time::Local> and L<Date::Parse> for conversion functions.
2294 I<template>, if specified, is the name of a suffix for alternate invoices.
2296 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2302 my( $today, $template, %opt );
2304 %opt = %{ shift() };
2305 $today = delete($opt{'time'}) || '';
2306 $template = delete($opt{template}) || '';
2308 ( $today, $template, %opt ) = @_;
2311 my %params = ( 'format' => 'template' );
2312 $params{'time'} = $today if $today;
2313 $params{'template'} = $template if $template;
2314 $params{$_} = $opt{$_}
2315 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2317 $self->print_generic( %params );
2320 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2322 Internal method - returns a filename of a filled-in LaTeX template for this
2323 invoice (Note: add ".tex" to get the actual filename), and a filename of
2324 an associated logo (with the .eps extension included).
2326 See print_ps and print_pdf for methods that return PostScript and PDF output.
2328 Options can be passed as a hashref (recommended) or as a list of time, template
2329 and then any key/value pairs for any other options.
2331 I<time>, if specified, is used to control the printing of overdue messages. The
2332 default is now. It isn't the date of the invoice; that's the `_date' field.
2333 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2334 L<Time::Local> and L<Date::Parse> for conversion functions.
2336 I<template>, if specified, is the name of a suffix for alternate invoices.
2338 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2344 my $conf = $self->conf;
2345 my( $today, $template, %opt );
2347 %opt = %{ shift() };
2348 $today = delete($opt{'time'}) || '';
2349 $template = delete($opt{template}) || '';
2351 ( $today, $template, %opt ) = @_;
2354 my %params = ( 'format' => 'latex' );
2355 $params{'time'} = $today if $today;
2356 $params{'template'} = $template if $template;
2357 $params{$_} = $opt{$_}
2358 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2360 $template ||= $self->_agent_template;
2362 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2363 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2367 ) or die "can't open temp file: $!\n";
2369 my $agentnum = $self->cust_main->agentnum;
2371 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2372 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2373 or die "can't write temp file: $!\n";
2375 print $lh $conf->config_binary('logo.eps', $agentnum)
2376 or die "can't write temp file: $!\n";
2379 $params{'logo_file'} = $lh->filename;
2381 if($conf->exists('invoice-barcode')){
2382 my $png_file = $self->invoice_barcode($dir);
2383 my $eps_file = $png_file;
2384 $eps_file =~ s/\.png$/.eps/g;
2385 $png_file =~ /(barcode.*png)/;
2387 $eps_file =~ /(barcode.*eps)/;
2390 my $curr_dir = cwd();
2392 # after painfuly long experimentation, it was determined that sam2p won't
2393 # accept : and other chars in the path, no matter how hard I tried to
2394 # escape them, hence the chdir (and chdir back, just to be safe)
2395 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2396 or die "sam2p failed: $!\n";
2400 $params{'barcode_file'} = $eps_file;
2403 my @filled_in = $self->print_generic( %params );
2405 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2409 ) or die "can't open temp file: $!\n";
2410 binmode($fh, ':utf8'); # language support
2411 print $fh join('', @filled_in );
2414 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2415 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2419 =item invoice_barcode DIR_OR_FALSE
2421 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2422 it is taken as the temp directory where the PNG file will be generated and the
2423 PNG file name is returned. Otherwise, the PNG image itself is returned.
2427 sub invoice_barcode {
2428 my ($self, $dir) = (shift,shift);
2430 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2431 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2432 my $gd = $gdbar->plot(Height => 30);
2435 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2439 ) or die "can't open temp file: $!\n";
2440 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2441 my $png_file = $bh->filename;
2448 =item print_generic OPTION => VALUE ...
2450 Internal method - returns a filled-in template for this invoice as a scalar.
2452 See print_ps and print_pdf for methods that return PostScript and PDF output.
2454 Non optional options include
2455 format - latex, html, template
2457 Optional options include
2459 template - a value used as a suffix for a configuration template
2461 time - a value used to control the printing of overdue messages. The
2462 default is now. It isn't the date of the invoice; that's the `_date' field.
2463 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2464 L<Time::Local> and L<Date::Parse> for conversion functions.
2468 unsquelch_cdr - overrides any per customer cdr squelching when true
2470 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2472 locale - override customer's locale
2476 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2477 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2478 # yes: fixed width/plain text printing will be borked
2480 my( $self, %params ) = @_;
2481 my $conf = $self->conf;
2482 my $today = $params{today} ? $params{today} : time;
2483 warn "$me print_generic called on $self with suffix $params{template}\n"
2486 my $format = $params{format};
2487 die "Unknown format: $format"
2488 unless $format =~ /^(latex|html|template)$/;
2490 my $cust_main = $self->cust_main;
2491 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2492 unless $cust_main->payname
2493 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2495 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2496 'html' => [ '<%=', '%>' ],
2497 'template' => [ '{', '}' ],
2500 warn "$me print_generic creating template\n"
2503 #create the template
2504 my $template = $params{template} ? $params{template} : $self->_agent_template;
2505 my $templatefile = "invoice_$format";
2506 $templatefile .= "_$template"
2507 if length($template) && $conf->exists($templatefile."_$template");
2508 my @invoice_template = map "$_\n", $conf->config($templatefile)
2509 or die "cannot load config data $templatefile";
2512 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2513 #change this to a die when the old code is removed
2514 warn "old-style invoice template $templatefile; ".
2515 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2516 $old_latex = 'true';
2517 @invoice_template = _translate_old_latex_format(@invoice_template);
2520 warn "$me print_generic creating T:T object\n"
2523 my $text_template = new Text::Template(
2525 SOURCE => \@invoice_template,
2526 DELIMITERS => $delimiters{$format},
2529 warn "$me print_generic compiling T:T object\n"
2532 $text_template->compile()
2533 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2536 # additional substitution could possibly cause breakage in existing templates
2537 my %convert_maps = (
2539 'notes' => sub { map "$_", @_ },
2540 'footer' => sub { map "$_", @_ },
2541 'smallfooter' => sub { map "$_", @_ },
2542 'returnaddress' => sub { map "$_", @_ },
2543 'coupon' => sub { map "$_", @_ },
2544 'summary' => sub { map "$_", @_ },
2550 s/%%(.*)$/<!-- $1 -->/g;
2551 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2552 s/\\begin\{enumerate\}/<ol>/g;
2554 s/\\end\{enumerate\}/<\/ol>/g;
2555 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2564 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2566 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2571 s/\\\\\*?\s*$/<BR>/;
2572 s/\\hyphenation\{[\w\s\-]+}//;
2577 'coupon' => sub { "" },
2578 'summary' => sub { "" },
2585 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2586 s/\\begin\{enumerate\}//g;
2588 s/\\end\{enumerate\}//g;
2589 s/\\textbf\{(.*)\}/$1/g;
2596 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2598 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2603 s/\\\\\*?\s*$/\n/; # dubious
2604 s/\\hyphenation\{[\w\s\-]+}//;
2608 'coupon' => sub { "" },
2609 'summary' => sub { "" },
2614 # hashes for differing output formats
2615 my %nbsps = ( 'latex' => '~',
2616 'html' => '', # '&nbps;' would be nice
2617 'template' => '', # not used
2619 my $nbsp = $nbsps{$format};
2621 my %escape_functions = ( 'latex' => \&_latex_escape,
2622 'html' => \&_html_escape_nbsp,#\&encode_entities,
2623 'template' => sub { shift },
2625 my $escape_function = $escape_functions{$format};
2626 my $escape_function_nonbsp = ($format eq 'html')
2627 ? \&_html_escape : $escape_function;
2629 my %date_formats = ( 'latex' => $date_format_long,
2630 'html' => $date_format_long,
2633 $date_formats{'html'} =~ s/ / /g;
2635 my $date_format = $date_formats{$format};
2637 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2639 'html' => sub { return '<b>'. shift(). '</b>'
2641 'template' => sub { shift },
2643 my $embolden_function = $embolden_functions{$format};
2645 my %newline_tokens = ( 'latex' => '\\\\',
2649 my $newline_token = $newline_tokens{$format};
2651 warn "$me generating template variables\n"
2654 # generate template variables
2657 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2661 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2667 $returnaddress = join("\n",
2668 $conf->config_orbase("invoice_${format}returnaddress", $template)
2671 } elsif ( grep /\S/,
2672 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2674 my $convert_map = $convert_maps{$format}{'returnaddress'};
2677 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2682 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2684 my $convert_map = $convert_maps{$format}{'returnaddress'};
2685 $returnaddress = join( "\n", &$convert_map(
2686 map { s/( {2,})/'~' x length($1)/eg;
2690 ( $conf->config('company_name', $self->cust_main->agentnum),
2691 $conf->config('company_address', $self->cust_main->agentnum),
2698 my $warning = "Couldn't find a return address; ".
2699 "do you need to set the company_address configuration value?";
2701 $returnaddress = $nbsp;
2702 #$returnaddress = $warning;
2706 warn "$me generating invoice data\n"
2709 my $agentnum = $self->cust_main->agentnum;
2711 my %invoice_data = (
2714 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2715 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2716 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2717 'returnaddress' => $returnaddress,
2718 'agent' => &$escape_function($cust_main->agent->agent),
2721 'invnum' => $self->invnum,
2722 'date' => time2str($date_format, $self->_date),
2723 'today' => time2str($date_format_long, $today),
2724 'terms' => $self->terms,
2725 'template' => $template, #params{'template'},
2726 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2727 'current_charges' => sprintf("%.2f", $self->charged),
2728 'duedate' => $self->due_date2str($rdate_format), #date_format?
2731 'custnum' => $cust_main->display_custnum,
2732 'agent_custid' => &$escape_function($cust_main->agent_custid),
2733 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2734 payname company address1 address2 city state zip fax
2738 'ship_enable' => $conf->exists('invoice-ship_address'),
2739 'unitprices' => $conf->exists('invoice-unitprice'),
2740 'smallernotes' => $conf->exists('invoice-smallernotes'),
2741 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2742 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2744 #layout info -- would be fancy to calc some of this and bury the template
2746 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2747 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2748 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2749 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2750 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2751 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2752 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2753 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2754 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2755 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2757 # better hang on to conf_dir for a while (for old templates)
2758 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2760 #these are only used when doing paged plaintext
2767 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2768 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2769 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2770 # eval to avoid death for unimplemented languages
2771 my $dh = eval { Date::Language->new($info{'name'}) } ||
2772 Date::Language->new(); # fall back to English
2773 # prototype here to silence warnings
2774 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2775 # eventually use this date handle everywhere in here, too
2777 my $min_sdate = 999999999999;
2779 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2780 next unless $cust_bill_pkg->pkgnum > 0;
2781 $min_sdate = $cust_bill_pkg->sdate
2782 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2783 $max_edate = $cust_bill_pkg->edate
2784 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2787 $invoice_data{'bill_period'} = '';
2788 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2789 . " to " . time2str('%e %h', $max_edate)
2790 if ($max_edate != 0 && $min_sdate != 999999999999);
2792 $invoice_data{finance_section} = '';
2793 if ( $conf->config('finance_pkgclass') ) {
2795 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2796 $invoice_data{finance_section} = $pkg_class->categoryname;
2798 $invoice_data{finance_amount} = '0.00';
2799 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2801 my $countrydefault = $conf->config('countrydefault') || 'US';
2802 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2803 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2804 my $method = $prefix.$_;
2805 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2807 $invoice_data{'ship_country'} = ''
2808 if ( $invoice_data{'ship_country'} eq $countrydefault );
2810 $invoice_data{'cid'} = $params{'cid'}
2813 if ( $cust_main->country eq $countrydefault ) {
2814 $invoice_data{'country'} = '';
2816 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2820 $invoice_data{'address'} = \@address;
2822 $cust_main->payname.
2823 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2824 ? " (P.O. #". $cust_main->payinfo. ")"
2828 push @address, $cust_main->company
2829 if $cust_main->company;
2830 push @address, $cust_main->address1;
2831 push @address, $cust_main->address2
2832 if $cust_main->address2;
2834 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2835 push @address, $invoice_data{'country'}
2836 if $invoice_data{'country'};
2838 while (scalar(@address) < 5);
2840 $invoice_data{'logo_file'} = $params{'logo_file'}
2841 if $params{'logo_file'};
2842 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2843 if $params{'barcode_file'};
2844 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2845 if $params{'barcode_img'};
2846 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2847 if $params{'barcode_cid'};
2849 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2850 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2851 #my $balance_due = $self->owed + $pr_total - $cr_total;
2852 my $balance_due = $self->owed + $pr_total;
2854 # the customer's current balance as shown on the invoice before this one
2855 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2857 # the change in balance from that invoice to this one
2858 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2860 # the sum of amount owed on all previous invoices
2861 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2863 # the sum of amount owed on all invoices
2864 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2866 # info from customer's last invoice before this one, for some
2868 $invoice_data{'last_bill'} = {};
2869 my $last_bill = $pr_cust_bill[-1];
2871 $invoice_data{'last_bill'} = {
2872 '_date' => $last_bill->_date, #unformatted
2873 # all we need for now
2877 my $summarypage = '';
2878 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2881 $invoice_data{'summarypage'} = $summarypage;
2883 warn "$me substituting variables in notes, footer, smallfooter\n"
2886 my @include = (qw( notes footer smallfooter ));
2887 push @include, 'coupon' unless $params{'no_coupon'};
2888 foreach my $include (@include) {
2890 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2893 if ( $conf->exists($inc_file, $agentnum)
2894 && length( $conf->config($inc_file, $agentnum) ) ) {
2896 @inc_src = $conf->config($inc_file, $agentnum);
2900 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2902 my $convert_map = $convert_maps{$format}{$include};
2904 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2905 s/--\@\]/$delimiters{$format}[1]/g;
2908 &$convert_map( $conf->config($inc_file, $agentnum) );
2912 my $inc_tt = new Text::Template (
2914 SOURCE => [ map "$_\n", @inc_src ],
2915 DELIMITERS => $delimiters{$format},
2916 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2918 unless ( $inc_tt->compile() ) {
2919 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2920 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2924 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2926 $invoice_data{$include} =~ s/\n+$//
2927 if ($format eq 'latex');
2930 # let invoices use either of these as needed
2931 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2932 ? $cust_main->payinfo : '';
2933 $invoice_data{'po_line'} =
2934 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2935 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2938 my %money_chars = ( 'latex' => '',
2939 'html' => $conf->config('money_char') || '$',
2942 my $money_char = $money_chars{$format};
2944 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2945 'html' => $conf->config('money_char') || '$',
2948 my $other_money_char = $other_money_chars{$format};
2949 $invoice_data{'dollar'} = $other_money_char;
2951 my @detail_items = ();
2952 my @total_items = ();
2956 $invoice_data{'detail_items'} = \@detail_items;
2957 $invoice_data{'total_items'} = \@total_items;
2958 $invoice_data{'buf'} = \@buf;
2959 $invoice_data{'sections'} = \@sections;
2961 warn "$me generating sections\n"
2964 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2965 'subtotal' => $other_money_char.
2966 sprintf('%.2f', $pr_total),
2967 'summarized' => '', #why? $summarypage ? 'Y' : '',
2969 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2970 join(' / ', map { $cust_main->balance_date_range(@$_) }
2971 $self->_prior_month30s
2973 if $conf->exists('invoice_include_aging');
2976 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2977 'subtotal' => $taxtotal, # adjusted below
2979 my $tax_weight = _pkg_category($tax_section->{description})
2980 ? _pkg_category($tax_section->{description})->weight
2982 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2983 $tax_section->{'sort_weight'} = $tax_weight;
2986 my $adjusttotal = 0;
2987 my $adjust_section = { 'description' =>
2988 $self->mt('Credits, Payments, and Adjustments'),
2989 'subtotal' => 0, # adjusted below
2991 my $adjust_weight = _pkg_category($adjust_section->{description})
2992 ? _pkg_category($adjust_section->{description})->weight
2994 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2995 $adjust_section->{'sort_weight'} = $adjust_weight;
2997 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2998 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2999 $invoice_data{'multisection'} = $multisection;
3000 my $late_sections = [];
3001 my $extra_sections = [];
3002 my $extra_lines = ();
3004 my $default_section = { 'description' => '',
3009 if ( $multisection ) {
3010 ($extra_sections, $extra_lines) =
3011 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3012 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3014 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3016 push @detail_items, @$extra_lines if $extra_lines;
3018 $self->_items_sections( $late_sections, # this could stand a refactor
3020 $escape_function_nonbsp,
3024 if ($conf->exists('svc_phone_sections')) {
3025 my ($phone_sections, $phone_lines) =
3026 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3027 push @{$late_sections}, @$phone_sections;
3028 push @detail_items, @$phone_lines;
3030 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3031 my ($accountcode_section, $accountcode_lines) =
3032 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3033 if ( scalar(@$accountcode_lines) ) {
3034 push @{$late_sections}, $accountcode_section;
3035 push @detail_items, @$accountcode_lines;
3038 } else {# not multisection
3039 # make a default section
3040 push @sections, $default_section;
3041 # and calculate the finance charge total, since it won't get done otherwise.
3042 # XXX possibly other totals?
3043 # XXX possibly finance_pkgclass should not be used in this manner?
3044 if ( $conf->exists('finance_pkgclass') ) {
3045 my @finance_charges;
3046 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3047 if ( grep { $_->section eq $invoice_data{finance_section} }
3048 $cust_bill_pkg->cust_bill_pkg_display ) {
3049 # I think these are always setup fees, but just to be sure...
3050 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3053 $invoice_data{finance_amount} =
3054 sprintf('%.2f', sum( @finance_charges ) || 0);
3058 # previous invoice balances in the Previous Charges section if there
3059 # is one, otherwise in the main detail section
3060 if ( $self->can('_items_previous') &&
3061 $self->enable_previous &&
3062 ! $conf->exists('previous_balance-summary_only') ) {
3064 warn "$me adding previous balances\n"
3067 foreach my $line_item ( $self->_items_previous ) {
3070 ext_description => [],
3072 $detail->{'ref'} = $line_item->{'pkgnum'};
3073 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3074 $detail->{'quantity'} = 1;
3075 $detail->{'section'} = $multisection ? $previous_section
3077 $detail->{'description'} = &$escape_function($line_item->{'description'});
3078 if ( exists $line_item->{'ext_description'} ) {
3079 @{$detail->{'ext_description'}} = map {
3080 &$escape_function($_);
3081 } @{$line_item->{'ext_description'}};
3083 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3084 $line_item->{'amount'};
3085 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3087 push @detail_items, $detail;
3088 push @buf, [ $detail->{'description'},
3089 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3095 if ( @pr_cust_bill && $self->enable_previous ) {
3096 push @buf, ['','-----------'];
3097 push @buf, [ $self->mt('Total Previous Balance'),
3098 $money_char. sprintf("%10.2f", $pr_total) ];
3102 if ( $conf->exists('svc_phone-did-summary') ) {
3103 warn "$me adding DID summary\n"
3106 my ($didsummary,$minutes) = $self->_did_summary;
3107 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3109 { 'description' => $didsummary_desc,
3110 'ext_description' => [ $didsummary, $minutes ],
3114 foreach my $section (@sections, @$late_sections) {
3116 warn "$me adding section \n". Dumper($section)
3119 # begin some normalization
3120 $section->{'subtotal'} = $section->{'amount'}
3122 && !exists($section->{subtotal})
3123 && exists($section->{amount});
3125 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3126 if ( $invoice_data{finance_section} &&
3127 $section->{'description'} eq $invoice_data{finance_section} );
3129 $section->{'subtotal'} = $other_money_char.
3130 sprintf('%.2f', $section->{'subtotal'})
3133 # continue some normalization
3134 $section->{'amount'} = $section->{'subtotal'}
3138 if ( $section->{'description'} ) {
3139 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3144 warn "$me setting options\n"
3147 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3149 $options{'section'} = $section if $multisection;
3150 $options{'format'} = $format;
3151 $options{'escape_function'} = $escape_function;
3152 $options{'no_usage'} = 1 unless $unsquelched;
3153 $options{'unsquelched'} = $unsquelched;
3154 $options{'summary_page'} = $summarypage;
3155 $options{'skip_usage'} =
3156 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3157 $options{'multilocation'} = $multilocation;
3158 $options{'multisection'} = $multisection;
3160 warn "$me searching for line items\n"
3163 foreach my $line_item ( $self->_items_pkg(%options) ) {
3165 warn "$me adding line item $line_item\n"
3169 ext_description => [],
3171 $detail->{'ref'} = $line_item->{'pkgnum'};
3172 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3173 $detail->{'quantity'} = $line_item->{'quantity'};
3174 $detail->{'section'} = $section;
3175 $detail->{'description'} = &$escape_function($line_item->{'description'});
3176 if ( exists $line_item->{'ext_description'} ) {
3177 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3179 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3180 $line_item->{'amount'};
3181 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3182 $line_item->{'unit_amount'};
3183 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3185 $detail->{'sdate'} = $line_item->{'sdate'};
3186 $detail->{'edate'} = $line_item->{'edate'};
3187 $detail->{'seconds'} = $line_item->{'seconds'};
3189 push @detail_items, $detail;
3190 push @buf, ( [ $detail->{'description'},
3191 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3193 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3197 if ( $section->{'description'} ) {
3198 push @buf, ( ['','-----------'],
3199 [ $section->{'description'}. ' sub-total',
3200 $section->{'subtotal'} # already formatted this
3209 $invoice_data{current_less_finance} =
3210 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3212 # create a major section for previous balance if we have major sections,
3213 # or if previous_section is in summary form
3214 if ( ( $multisection && $self->enable_previous )
3215 || $conf->exists('previous_balance-summary_only') )
3217 unshift @sections, $previous_section if $pr_total;
3220 warn "$me adding taxes\n"
3223 foreach my $tax ( $self->_items_tax ) {
3225 $taxtotal += $tax->{'amount'};
3227 my $description = &$escape_function( $tax->{'description'} );
3228 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3230 if ( $multisection ) {
3232 my $money = $old_latex ? '' : $money_char;
3233 push @detail_items, {
3234 ext_description => [],
3237 description => $description,
3238 amount => $money. $amount,
3240 section => $tax_section,
3245 push @total_items, {
3246 'total_item' => $description,
3247 'total_amount' => $other_money_char. $amount,
3252 push @buf,[ $description,
3253 $money_char. $amount,
3260 $total->{'total_item'} = $self->mt('Sub-total');
3261 $total->{'total_amount'} =
3262 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3264 if ( $multisection ) {
3265 $tax_section->{'subtotal'} = $other_money_char.
3266 sprintf('%.2f', $taxtotal);
3267 $tax_section->{'pretotal'} = 'New charges sub-total '.
3268 $total->{'total_amount'};
3269 push @sections, $tax_section if $taxtotal;
3271 unshift @total_items, $total;
3274 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3276 push @buf,['','-----------'];
3277 push @buf,[$self->mt(
3278 (!$self->enable_previous)
3280 : 'Total New Charges'
3282 $money_char. sprintf("%10.2f",$self->charged) ];
3285 # calculate total, possibly including total owed on previous
3290 $item = $conf->config('previous_balance-exclude_from_total')
3291 || 'Total New Charges'
3292 if $conf->exists('previous_balance-exclude_from_total');
3293 my $amount = $self->charged;
3294 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3295 $amount += $pr_total;
3298 $total->{'total_item'} = &$embolden_function($self->mt($item));
3299 $total->{'total_amount'} =
3300 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3301 if ( $multisection ) {
3302 if ( $adjust_section->{'sort_weight'} ) {
3303 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3304 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3306 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3307 $other_money_char. sprintf('%.2f', $self->charged );
3310 push @total_items, $total;
3312 push @buf,['','-----------'];
3315 sprintf( '%10.2f', $amount )
3320 # if we're showing previous invoices, also show previous
3321 # credits and payments
3322 if ( $self->enable_previous
3323 and $self->can('_items_credits')
3324 and $self->can('_items_payments') )
3326 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3329 my $credittotal = 0;
3330 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3333 $total->{'total_item'} = &$escape_function($credit->{'description'});
3334 $credittotal += $credit->{'amount'};
3335 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3336 $adjusttotal += $credit->{'amount'};
3337 if ( $multisection ) {
3338 my $money = $old_latex ? '' : $money_char;
3339 push @detail_items, {
3340 ext_description => [],
3343 description => &$escape_function($credit->{'description'}),
3344 amount => $money. $credit->{'amount'},
3346 section => $adjust_section,
3349 push @total_items, $total;
3353 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3356 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3357 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3361 my $paymenttotal = 0;
3362 foreach my $payment ( $self->_items_payments ) {
3364 $total->{'total_item'} = &$escape_function($payment->{'description'});
3365 $paymenttotal += $payment->{'amount'};
3366 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3367 $adjusttotal += $payment->{'amount'};
3368 if ( $multisection ) {
3369 my $money = $old_latex ? '' : $money_char;
3370 push @detail_items, {
3371 ext_description => [],
3374 description => &$escape_function($payment->{'description'}),
3375 amount => $money. $payment->{'amount'},
3377 section => $adjust_section,
3380 push @total_items, $total;
3382 push @buf, [ $payment->{'description'},
3383 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3386 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3388 if ( $multisection ) {
3389 $adjust_section->{'subtotal'} = $other_money_char.
3390 sprintf('%.2f', $adjusttotal);
3391 push @sections, $adjust_section
3392 unless $adjust_section->{sort_weight};
3395 # create Balance Due message
3398 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3399 $total->{'total_amount'} =
3400 &$embolden_function(
3401 $other_money_char. sprintf('%.2f', $summarypage
3403 $self->billing_balance
3404 : $self->owed + $pr_total
3407 if ( $multisection && !$adjust_section->{sort_weight} ) {
3408 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3409 $total->{'total_amount'};
3411 push @total_items, $total;
3413 push @buf,['','-----------'];
3414 push @buf,[$self->balance_due_msg, $money_char.
3415 sprintf("%10.2f", $balance_due ) ];
3418 if ( $conf->exists('previous_balance-show_credit')
3419 and $cust_main->balance < 0 ) {
3420 my $credit_total = {
3421 'total_item' => &$embolden_function($self->credit_balance_msg),
3422 'total_amount' => &$embolden_function(
3423 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3426 if ( $multisection ) {
3427 $adjust_section->{'posttotal'} .= $newline_token .
3428 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3431 push @total_items, $credit_total;
3433 push @buf,['','-----------'];
3434 push @buf,[$self->credit_balance_msg, $money_char.
3435 sprintf("%10.2f", -$cust_main->balance ) ];
3439 if ( $multisection ) {
3440 if ($conf->exists('svc_phone_sections')) {
3442 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3443 $total->{'total_amount'} =
3444 &$embolden_function(
3445 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3447 my $last_section = pop @sections;
3448 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3449 $total->{'total_amount'};
3450 push @sections, $last_section;
3452 push @sections, @$late_sections
3456 # make a discounts-available section, even without multisection
3457 if ( $conf->exists('discount-show_available')
3458 and my @discounts_avail = $self->_items_discounts_avail ) {
3459 my $discount_section = {
3460 'description' => $self->mt('Discounts Available'),
3465 push @sections, $discount_section;
3466 push @detail_items, map { +{
3467 'ref' => '', #should this be something else?
3468 'section' => $discount_section,
3469 'description' => &$escape_function( $_->{description} ),
3470 'amount' => $money_char . &$escape_function( $_->{amount} ),
3471 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3472 } } @discounts_avail;
3475 # All sections and items are built; now fill in templates.
3476 my @includelist = ();
3477 push @includelist, 'summary' if $summarypage;
3478 foreach my $include ( @includelist ) {
3480 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3483 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3485 @inc_src = $conf->config($inc_file, $agentnum);
3489 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3491 my $convert_map = $convert_maps{$format}{$include};
3493 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3494 s/--\@\]/$delimiters{$format}[1]/g;
3497 &$convert_map( $conf->config($inc_file, $agentnum) );
3501 my $inc_tt = new Text::Template (
3503 SOURCE => [ map "$_\n", @inc_src ],
3504 DELIMITERS => $delimiters{$format},
3505 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3507 unless ( $inc_tt->compile() ) {
3508 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3509 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3513 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3515 $invoice_data{$include} =~ s/\n+$//
3516 if ($format eq 'latex');
3521 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3522 /invoice_lines\((\d*)\)/;
3523 $invoice_lines += $1 || scalar(@buf);
3526 die "no invoice_lines() functions in template?"
3527 if ( $format eq 'template' && !$wasfunc );
3529 if ($format eq 'template') {
3531 if ( $invoice_lines ) {
3532 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3533 $invoice_data{'total_pages'}++
3534 if scalar(@buf) % $invoice_lines;
3537 #setup subroutine for the template
3538 $invoice_data{invoice_lines} = sub {
3539 my $lines = shift || scalar(@buf);
3551 push @collect, split("\n",
3552 $text_template->fill_in( HASH => \%invoice_data )
3554 $invoice_data{'page'}++;
3556 map "$_\n", @collect;
3558 # this is where we actually create the invoice
3559 warn "filling in template for invoice ". $self->invnum. "\n"
3561 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3564 $text_template->fill_in(HASH => \%invoice_data);
3568 # helper routine for generating date ranges
3569 sub _prior_month30s {
3572 [ 1, 2592000 ], # 0-30 days ago
3573 [ 2592000, 5184000 ], # 30-60 days ago
3574 [ 5184000, 7776000 ], # 60-90 days ago
3575 [ 7776000, 0 ], # 90+ days ago
3578 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3579 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3584 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3586 Returns an postscript invoice, as a scalar.
3588 Options can be passed as a hashref (recommended) or as a list of time, template
3589 and then any key/value pairs for any other options.
3591 I<time> an optional value used to control the printing of overdue messages. The
3592 default is now. It isn't the date of the invoice; that's the `_date' field.
3593 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3594 L<Time::Local> and L<Date::Parse> for conversion functions.
3596 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3603 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3604 my $ps = generate_ps($file);
3606 unlink($barcodefile) if $barcodefile;
3611 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3613 Returns an PDF invoice, as a scalar.
3615 Options can be passed as a hashref (recommended) or as a list of time, template
3616 and then any key/value pairs for any other options.
3618 I<time> an optional value used to control the printing of overdue messages. The
3619 default is now. It isn't the date of the invoice; that's the `_date' field.
3620 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3621 L<Time::Local> and L<Date::Parse> for conversion functions.
3623 I<template>, if specified, is the name of a suffix for alternate invoices.
3625 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3632 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3633 my $pdf = generate_pdf($file);
3635 unlink($barcodefile) if $barcodefile;
3640 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3642 Returns an HTML invoice, as a scalar.
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)
3653 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3654 when emailing the invoice as part of a multipart/related MIME email.
3662 %params = %{ shift() };
3664 $params{'time'} = shift;
3665 $params{'template'} = shift;
3666 $params{'cid'} = shift;
3669 $params{'format'} = 'html';
3671 $self->print_generic( %params );
3674 # quick subroutine for print_latex
3676 # There are ten characters that LaTeX treats as special characters, which
3677 # means that they do not simply typeset themselves:
3678 # # $ % & ~ _ ^ \ { }
3680 # TeX ignores blanks following an escaped character; if you want a blank (as
3681 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3685 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3686 $value =~ s/([<>])/\$$1\$/g;
3692 encode_entities($value);
3696 sub _html_escape_nbsp {
3697 my $value = _html_escape(shift);
3698 $value =~ s/ +/ /g;
3702 #utility methods for print_*
3704 sub _translate_old_latex_format {
3705 warn "_translate_old_latex_format called\n"
3712 if ( $line =~ /^%%Detail\s*$/ ) {
3714 push @template, q![@--!,
3715 q! foreach my $_tr_line (@detail_items) {!,
3716 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3717 q! $_tr_line->{'description'} .= !,
3718 q! "\\tabularnewline\n~~".!,
3719 q! join( "\\tabularnewline\n~~",!,
3720 q! @{$_tr_line->{'ext_description'}}!,
3724 while ( ( my $line_item_line = shift )
3725 !~ /^%%EndDetail\s*$/ ) {
3726 $line_item_line =~ s/'/\\'/g; # nice LTS
3727 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3728 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3729 push @template, " \$OUT .= '$line_item_line';";
3732 push @template, '}',
3735 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3737 push @template, '[@--',
3738 ' foreach my $_tr_line (@total_items) {';
3740 while ( ( my $total_item_line = shift )
3741 !~ /^%%EndTotalDetails\s*$/ ) {
3742 $total_item_line =~ s/'/\\'/g; # nice LTS
3743 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3744 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3745 push @template, " \$OUT .= '$total_item_line';";
3748 push @template, '}',
3752 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3753 push @template, $line;
3759 warn "$_\n" foreach @template;
3767 my $conf = $self->conf;
3769 #check for an invoice-specific override
3770 return $self->invoice_terms if $self->invoice_terms;
3772 #check for a customer- specific override
3773 my $cust_main = $self->cust_main;
3774 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3776 #use configured default
3777 $conf->config('invoice_default_terms') || '';
3783 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3784 $duedate = $self->_date() + ( $1 * 86400 );
3791 $self->due_date ? time2str(shift, $self->due_date) : '';
3794 sub balance_due_msg {
3796 my $msg = $self->mt('Balance Due');
3797 return $msg unless $self->terms;
3798 if ( $self->due_date ) {
3799 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3800 $self->due_date2str($date_format);
3801 } elsif ( $self->terms ) {
3802 $msg .= ' - '. $self->terms;
3807 sub balance_due_date {
3809 my $conf = $self->conf;
3811 if ( $conf->exists('invoice_default_terms')
3812 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3813 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3818 sub credit_balance_msg {
3820 $self->mt('Credit Balance Remaining')
3823 =item invnum_date_pretty
3825 Returns a string with the invoice number and date, for example:
3826 "Invoice #54 (3/20/2008)"
3830 sub invnum_date_pretty {
3832 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3837 Returns a string with the date, for example: "3/20/2008"
3843 time2str($date_format, $self->_date);
3846 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3848 Generate section information for all items appearing on this invoice.
3849 This will only be called for multi-section invoices.
3851 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3852 related display records (L<FS::cust_bill_pkg_display>) and organize
3853 them into two groups ("early" and "late" according to whether they come
3854 before or after the total), then into sections. A subtotal is calculated
3857 Section descriptions are returned in sort weight order. Each consists
3858 of a hash containing:
3860 description: the package category name, escaped
3861 subtotal: the total charges in that section
3862 tax_section: a flag indicating that the section contains only tax charges
3863 summarized: same as tax_section, for some reason
3864 sort_weight: the package category's sort weight
3866 If 'condense' is set on the display record, it also contains everything
3867 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3868 coderefs to generate parts of the invoice. This is not advised.
3872 LATE: an arrayref to push the "late" section hashes onto. The "early"
3873 group is simply returned from the method.
3875 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3876 Turning this on has the following effects:
3877 - Ignores display items with the 'summary' flag.
3878 - Combines all items into the "early" group.
3879 - Creates sections for all non-disabled package categories, even if they
3880 have no charges on this invoice, as well as a section with no name.
3882 ESCAPE: an escape function to use for section titles.
3884 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3885 sorted list. If there are any of these, section subtotals exclude
3888 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3889 passed through to C<_condense_section()>.
3893 use vars qw(%pkg_category_cache);
3894 sub _items_sections {
3897 my $summarypage = shift;
3899 my $extra_sections = shift;
3903 my %late_subtotal = ();
3906 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3909 my $usage = $cust_bill_pkg->usage;
3911 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3912 next if ( $display->summary && $summarypage );
3914 my $section = $display->section;
3915 my $type = $display->type;
3917 $not_tax{$section} = 1
3918 unless $cust_bill_pkg->pkgnum == 0;
3920 if ( $display->post_total && !$summarypage ) {
3921 if (! $type || $type eq 'S') {
3922 $late_subtotal{$section} += $cust_bill_pkg->setup
3923 if $cust_bill_pkg->setup != 0
3924 || $cust_bill_pkg->setup_show_zero;
3928 $late_subtotal{$section} += $cust_bill_pkg->recur
3929 if $cust_bill_pkg->recur != 0
3930 || $cust_bill_pkg->recur_show_zero;
3933 if ($type && $type eq 'R') {
3934 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3935 if $cust_bill_pkg->recur != 0
3936 || $cust_bill_pkg->recur_show_zero;
3939 if ($type && $type eq 'U') {
3940 $late_subtotal{$section} += $usage
3941 unless scalar(@$extra_sections);
3946 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3948 if (! $type || $type eq 'S') {
3949 $subtotal{$section} += $cust_bill_pkg->setup
3950 if $cust_bill_pkg->setup != 0
3951 || $cust_bill_pkg->setup_show_zero;
3955 $subtotal{$section} += $cust_bill_pkg->recur
3956 if $cust_bill_pkg->recur != 0
3957 || $cust_bill_pkg->recur_show_zero;
3960 if ($type && $type eq 'R') {
3961 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3962 if $cust_bill_pkg->recur != 0
3963 || $cust_bill_pkg->recur_show_zero;
3966 if ($type && $type eq 'U') {
3967 $subtotal{$section} += $usage
3968 unless scalar(@$extra_sections);
3977 %pkg_category_cache = ();
3979 push @$late, map { { 'description' => &{$escape}($_),
3980 'subtotal' => $late_subtotal{$_},
3982 'sort_weight' => ( _pkg_category($_)
3983 ? _pkg_category($_)->weight
3986 ((_pkg_category($_) && _pkg_category($_)->condense)
3987 ? $self->_condense_section($format)
3991 sort _sectionsort keys %late_subtotal;
3994 if ( $summarypage ) {
3995 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3996 map { $_->categoryname } qsearch('pkg_category', {});
3997 push @sections, '' if exists($subtotal{''});
3999 @sections = keys %subtotal;
4002 my @early = map { { 'description' => &{$escape}($_),
4003 'subtotal' => $subtotal{$_},
4004 'summarized' => $not_tax{$_} ? '' : 'Y',
4005 'tax_section' => $not_tax{$_} ? '' : 'Y',
4006 'sort_weight' => ( _pkg_category($_)
4007 ? _pkg_category($_)->weight
4010 ((_pkg_category($_) && _pkg_category($_)->condense)
4011 ? $self->_condense_section($format)
4016 push @early, @$extra_sections if $extra_sections;
4018 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4022 #helper subs for above
4025 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4029 my $categoryname = shift;
4030 $pkg_category_cache{$categoryname} ||=
4031 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4034 my %condensed_format = (
4035 'label' => [ qw( Description Qty Amount ) ],
4037 sub { shift->{description} },
4038 sub { shift->{quantity} },
4039 sub { my($href, %opt) = @_;
4040 ($opt{dollar} || ''). $href->{amount};
4043 'align' => [ qw( l r r ) ],
4044 'span' => [ qw( 5 1 1 ) ], # unitprices?
4045 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4048 sub _condense_section {
4049 my ( $self, $format ) = ( shift, shift );
4051 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4052 qw( description_generator
4055 total_line_generator
4060 sub _condensed_generator_defaults {
4061 my ( $self, $format ) = ( shift, shift );
4062 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4071 sub _condensed_header_generator {
4072 my ( $self, $format ) = ( shift, shift );
4074 my ( $f, $prefix, $suffix, $separator, $column ) =
4075 _condensed_generator_defaults($format);
4077 if ($format eq 'latex') {
4078 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4079 $suffix = "\\\\\n\\hline";
4082 sub { my ($d,$a,$s,$w) = @_;
4083 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4085 } elsif ( $format eq 'html' ) {
4086 $prefix = '<th></th>';
4090 sub { my ($d,$a,$s,$w) = @_;
4091 return qq!<th align="$html_align{$a}">$d</th>!;
4099 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4101 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4104 $prefix. join($separator, @result). $suffix;
4109 sub _condensed_description_generator {
4110 my ( $self, $format ) = ( shift, shift );
4112 my ( $f, $prefix, $suffix, $separator, $column ) =
4113 _condensed_generator_defaults($format);
4115 my $money_char = '$';
4116 if ($format eq 'latex') {
4117 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4119 $separator = " & \n";
4121 sub { my ($d,$a,$s,$w) = @_;
4122 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4124 $money_char = '\\dollar';
4125 }elsif ( $format eq 'html' ) {
4126 $prefix = '"><td align="center"></td>';
4130 sub { my ($d,$a,$s,$w) = @_;
4131 return qq!<td align="$html_align{$a}">$d</td>!;
4133 #$money_char = $conf->config('money_char') || '$';
4134 $money_char = ''; # this is madness
4142 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4144 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4146 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4147 map { $f->{$_}->[$i] } qw(align span width)
4151 $prefix. join( $separator, @result ). $suffix;
4156 sub _condensed_total_generator {
4157 my ( $self, $format ) = ( shift, shift );
4159 my ( $f, $prefix, $suffix, $separator, $column ) =
4160 _condensed_generator_defaults($format);
4163 if ($format eq 'latex') {
4166 $separator = " & \n";
4168 sub { my ($d,$a,$s,$w) = @_;
4169 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4171 }elsif ( $format eq 'html' ) {
4175 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4177 sub { my ($d,$a,$s,$w) = @_;
4178 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4187 # my $r = &{$f->{fields}->[$i]}(@args);
4188 # $r .= ' Total' unless $i;
4190 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4192 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4193 map { $f->{$_}->[$i] } qw(align span width)
4197 $prefix. join( $separator, @result ). $suffix;
4202 =item total_line_generator FORMAT
4204 Returns a coderef used for generation of invoice total line items for this
4205 usage_class. FORMAT is either html or latex
4209 # should not be used: will have issues with hash element names (description vs
4210 # total_item and amount vs total_amount -- another array of functions?
4212 sub _condensed_total_line_generator {
4213 my ( $self, $format ) = ( shift, shift );
4215 my ( $f, $prefix, $suffix, $separator, $column ) =
4216 _condensed_generator_defaults($format);
4219 if ($format eq 'latex') {
4222 $separator = " & \n";
4224 sub { my ($d,$a,$s,$w) = @_;
4225 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4227 }elsif ( $format eq 'html' ) {
4231 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4233 sub { my ($d,$a,$s,$w) = @_;
4234 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4243 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4245 &{$column}( &{$f->{fields}->[$i]}(@args),
4246 map { $f->{$_}->[$i] } qw(align span width)
4250 $prefix. join( $separator, @result ). $suffix;
4255 #sub _items_extra_usage_sections {
4257 # my $escape = shift;
4259 # my %sections = ();
4261 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4262 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4264 # next unless $cust_bill_pkg->pkgnum > 0;
4266 # foreach my $section ( keys %usage_class ) {
4268 # my $usage = $cust_bill_pkg->usage($section);
4270 # next unless $usage && $usage > 0;
4272 # $sections{$section} ||= 0;
4273 # $sections{$section} += $usage;
4279 # map { { 'description' => &{$escape}($_),
4280 # 'subtotal' => $sections{$_},
4281 # 'summarized' => '',
4282 # 'tax_section' => '',
4285 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4289 sub _items_extra_usage_sections {
4291 my $conf = $self->conf;
4299 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4301 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4302 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4303 next unless $cust_bill_pkg->pkgnum > 0;
4305 foreach my $classnum ( keys %usage_class ) {
4306 my $section = $usage_class{$classnum}->classname;
4307 $classnums{$section} = $classnum;
4309 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4310 my $amount = $detail->amount;
4311 next unless $amount && $amount > 0;
4313 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4314 $sections{$section}{amount} += $amount; #subtotal
4315 $sections{$section}{calls}++;
4316 $sections{$section}{duration} += $detail->duration;
4318 my $desc = $detail->regionname;
4319 my $description = $desc;
4320 $description = substr($desc, 0, $maxlength). '...'
4321 if $format eq 'latex' && length($desc) > $maxlength;
4323 $lines{$section}{$desc} ||= {
4324 description => &{$escape}($description),
4325 #pkgpart => $part_pkg->pkgpart,
4326 pkgnum => $cust_bill_pkg->pkgnum,
4331 #unit_amount => $cust_bill_pkg->unitrecur,
4332 quantity => $cust_bill_pkg->quantity,
4333 product_code => 'N/A',
4334 ext_description => [],
4337 $lines{$section}{$desc}{amount} += $amount;
4338 $lines{$section}{$desc}{calls}++;
4339 $lines{$section}{$desc}{duration} += $detail->duration;
4345 my %sectionmap = ();
4346 foreach (keys %sections) {
4347 my $usage_class = $usage_class{$classnums{$_}};
4348 $sectionmap{$_} = { 'description' => &{$escape}($_),
4349 'amount' => $sections{$_}{amount}, #subtotal
4350 'calls' => $sections{$_}{calls},
4351 'duration' => $sections{$_}{duration},
4353 'tax_section' => '',
4354 'sort_weight' => $usage_class->weight,
4355 ( $usage_class->format
4356 ? ( map { $_ => $usage_class->$_($format) }
4357 qw( description_generator header_generator total_generator total_line_generator )
4364 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4368 foreach my $section ( keys %lines ) {
4369 foreach my $line ( keys %{$lines{$section}} ) {
4370 my $l = $lines{$section}{$line};
4371 $l->{section} = $sectionmap{$section};
4372 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4373 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4378 return(\@sections, \@lines);
4384 my $end = $self->_date;
4386 # start at date of previous invoice + 1 second or 0 if no previous invoice
4387 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4388 $start = 0 if !$start;
4391 my $cust_main = $self->cust_main;
4392 my @pkgs = $cust_main->all_pkgs;
4393 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4396 foreach my $pkg ( @pkgs ) {
4397 my @h_cust_svc = $pkg->h_cust_svc($end);
4398 foreach my $h_cust_svc ( @h_cust_svc ) {
4399 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4400 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4402 my $inserted = $h_cust_svc->date_inserted;
4403 my $deleted = $h_cust_svc->date_deleted;
4404 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4406 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4408 # DID either activated or ported in; cannot be both for same DID simultaneously
4409 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4410 && (!$phone_inserted->lnp_status
4411 || $phone_inserted->lnp_status eq ''
4412 || $phone_inserted->lnp_status eq 'native')) {
4415 else { # this one not so clean, should probably move to (h_)svc_phone
4416 my $phone_portedin = qsearchs( 'h_svc_phone',
4417 { 'svcnum' => $h_cust_svc->svcnum,
4418 'lnp_status' => 'portedin' },
4419 FS::h_svc_phone->sql_h_searchs($end),
4421 $num_portedin++ if $phone_portedin;
4424 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4425 if($deleted >= $start && $deleted <= $end && $phone_deleted
4426 && (!$phone_deleted->lnp_status
4427 || $phone_deleted->lnp_status ne 'portingout')) {
4430 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4431 && $phone_deleted->lnp_status
4432 && $phone_deleted->lnp_status eq 'portingout') {
4436 # increment usage minutes
4437 if ( $phone_inserted ) {
4438 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4439 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4442 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4445 # don't look at this service again
4446 push @seen, $h_cust_svc->svcnum;
4450 $minutes = sprintf("%d", $minutes);
4451 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4452 . "$num_deactivated Ported-Out: $num_portedout ",
4453 "Total Minutes: $minutes");
4456 sub _items_accountcode_cdr {
4461 my $section = { 'amount' => 0,
4464 'sort_weight' => '',
4466 'description' => 'Usage by Account Code',
4472 my %accountcodes = ();
4474 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4475 next unless $cust_bill_pkg->pkgnum > 0;
4477 my @header = $cust_bill_pkg->details_header;
4478 next unless scalar(@header);
4479 $section->{'header'} = join(',',@header);
4481 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4483 $section->{'header'} = $detail->formatted('format' => $format)
4484 if($detail->detail eq $section->{'header'});
4486 my $accountcode = $detail->accountcode;
4487 next unless $accountcode;
4489 my $amount = $detail->amount;
4490 next unless $amount && $amount > 0;
4492 $accountcodes{$accountcode} ||= {
4493 description => $accountcode,
4500 product_code => 'N/A',
4501 section => $section,
4502 ext_description => [ $section->{'header'} ],
4506 $section->{'amount'} += $amount;
4507 $accountcodes{$accountcode}{'amount'} += $amount;
4508 $accountcodes{$accountcode}{calls}++;
4509 $accountcodes{$accountcode}{duration} += $detail->duration;
4510 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4514 foreach my $l ( values %accountcodes ) {
4515 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4516 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4517 foreach my $sorted_detail ( @sorted_detail ) {
4518 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4520 delete $l->{detail_temp};
4524 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4526 return ($section,\@sorted_lines);
4529 sub _items_svc_phone_sections {
4531 my $conf = $self->conf;
4539 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4541 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4542 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4544 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4545 next unless $cust_bill_pkg->pkgnum > 0;
4547 my @header = $cust_bill_pkg->details_header;
4548 next unless scalar(@header);
4550 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4552 my $phonenum = $detail->phonenum;
4553 next unless $phonenum;
4555 my $amount = $detail->amount;
4556 next unless $amount && $amount > 0;
4558 $sections{$phonenum} ||= { 'amount' => 0,
4561 'sort_weight' => -1,
4562 'phonenum' => $phonenum,
4564 $sections{$phonenum}{amount} += $amount; #subtotal
4565 $sections{$phonenum}{calls}++;
4566 $sections{$phonenum}{duration} += $detail->duration;
4568 my $desc = $detail->regionname;
4569 my $description = $desc;
4570 $description = substr($desc, 0, $maxlength). '...'
4571 if $format eq 'latex' && length($desc) > $maxlength;
4573 $lines{$phonenum}{$desc} ||= {
4574 description => &{$escape}($description),
4575 #pkgpart => $part_pkg->pkgpart,
4583 product_code => 'N/A',
4584 ext_description => [],
4587 $lines{$phonenum}{$desc}{amount} += $amount;
4588 $lines{$phonenum}{$desc}{calls}++;
4589 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4591 my $line = $usage_class{$detail->classnum}->classname;
4592 $sections{"$phonenum $line"} ||=
4596 'sort_weight' => $usage_class{$detail->classnum}->weight,
4597 'phonenum' => $phonenum,
4598 'header' => [ @header ],
4600 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4601 $sections{"$phonenum $line"}{calls}++;
4602 $sections{"$phonenum $line"}{duration} += $detail->duration;
4604 $lines{"$phonenum $line"}{$desc} ||= {
4605 description => &{$escape}($description),
4606 #pkgpart => $part_pkg->pkgpart,
4614 product_code => 'N/A',
4615 ext_description => [],
4618 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4619 $lines{"$phonenum $line"}{$desc}{calls}++;
4620 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4621 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4622 $detail->formatted('format' => $format);
4627 my %sectionmap = ();
4628 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4629 foreach ( keys %sections ) {
4630 my @header = @{ $sections{$_}{header} || [] };
4632 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4633 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4634 my $usage_class = $summary ? $simple : $usage_simple;
4635 my $ending = $summary ? ' usage charges' : '';
4638 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4640 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4641 'amount' => $sections{$_}{amount}, #subtotal
4642 'calls' => $sections{$_}{calls},
4643 'duration' => $sections{$_}{duration},
4645 'tax_section' => '',
4646 'phonenum' => $sections{$_}{phonenum},
4647 'sort_weight' => $sections{$_}{sort_weight},
4648 'post_total' => $summary, #inspire pagebreak
4650 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4651 qw( description_generator
4654 total_line_generator
4661 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4662 $a->{sort_weight} <=> $b->{sort_weight}
4667 foreach my $section ( keys %lines ) {
4668 foreach my $line ( keys %{$lines{$section}} ) {
4669 my $l = $lines{$section}{$line};
4670 $l->{section} = $sectionmap{$section};
4671 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4672 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4677 if($conf->exists('phone_usage_class_summary')) {
4678 # this only works with Latex
4682 # after this, we'll have only two sections per DID:
4683 # Calls Summary and Calls Detail
4684 foreach my $section ( @sections ) {
4685 if($section->{'post_total'}) {
4686 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4687 $section->{'total_line_generator'} = sub { '' };
4688 $section->{'total_generator'} = sub { '' };
4689 $section->{'header_generator'} = sub { '' };
4690 $section->{'description_generator'} = '';
4691 push @newsections, $section;
4692 my %calls_detail = %$section;
4693 $calls_detail{'post_total'} = '';
4694 $calls_detail{'sort_weight'} = '';
4695 $calls_detail{'description_generator'} = sub { '' };
4696 $calls_detail{'header_generator'} = sub {
4697 return ' & Date/Time & Called Number & Duration & Price'
4698 if $format eq 'latex';
4701 $calls_detail{'description'} = 'Calls Detail: '
4702 . $section->{'phonenum'};
4703 push @newsections, \%calls_detail;
4707 # after this, each usage class is collapsed/summarized into a single
4708 # line under the Calls Summary section
4709 foreach my $newsection ( @newsections ) {
4710 if($newsection->{'post_total'}) { # this means Calls Summary
4711 foreach my $section ( @sections ) {
4712 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4713 && !$section->{'post_total'});
4714 my $newdesc = $section->{'description'};
4715 my $tn = $section->{'phonenum'};
4716 $newdesc =~ s/$tn//g;
4717 my $line = { ext_description => [],
4721 calls => $section->{'calls'},
4722 section => $newsection,
4723 duration => $section->{'duration'},
4724 description => $newdesc,
4725 amount => sprintf("%.2f",$section->{'amount'}),
4726 product_code => 'N/A',
4728 push @newlines, $line;
4733 # after this, Calls Details is populated with all CDRs
4734 foreach my $newsection ( @newsections ) {
4735 if(!$newsection->{'post_total'}) { # this means Calls Details
4736 foreach my $line ( @lines ) {
4737 next unless (scalar(@{$line->{'ext_description'}}) &&
4738 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4740 my @extdesc = @{$line->{'ext_description'}};
4742 foreach my $extdesc ( @extdesc ) {
4743 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4744 push @newextdesc, $extdesc;
4746 $line->{'ext_description'} = \@newextdesc;
4747 $line->{'section'} = $newsection;
4748 push @newlines, $line;
4753 return(\@newsections, \@newlines);
4756 return(\@sections, \@lines);
4760 sub _items { # seems to be unused
4763 #my @display = scalar(@_)
4765 # : qw( _items_previous _items_pkg );
4766 # #: qw( _items_pkg );
4767 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4768 my @display = qw( _items_previous _items_pkg );
4771 foreach my $display ( @display ) {
4772 push @b, $self->$display(@_);
4777 sub _items_previous {
4779 my $conf = $self->conf;
4780 my $cust_main = $self->cust_main;
4781 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4783 foreach ( @pr_cust_bill ) {
4784 my $date = $conf->exists('invoice_show_prior_due_date')
4785 ? 'due '. $_->due_date2str($date_format)
4786 : time2str($date_format, $_->_date);
4788 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4789 #'pkgpart' => 'N/A',
4791 'amount' => sprintf("%.2f", $_->owed),
4797 # 'description' => 'Previous Balance',
4798 # #'pkgpart' => 'N/A',
4799 # 'pkgnum' => 'N/A',
4800 # 'amount' => sprintf("%10.2f", $pr_total ),
4801 # 'ext_description' => [ map {
4802 # "Invoice ". $_->invnum.
4803 # " (". time2str("%x",$_->_date). ") ".
4804 # sprintf("%10.2f", $_->owed)
4805 # } @pr_cust_bill ],
4810 =item _items_pkg [ OPTIONS ]
4812 Return line item hashes for each package item on this invoice. Nearly
4815 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4817 The only OPTIONS accepted is 'section', which may point to a hashref
4818 with a key named 'condensed', which may have a true value. If it
4819 does, this method tries to merge identical items into items with
4820 'quantity' equal to the number of items (not the sum of their
4821 separate quantities, for some reason).
4829 warn "$me _items_pkg searching for all package line items\n"
4832 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4834 warn "$me _items_pkg filtering line items\n"
4836 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4838 if ($options{section} && $options{section}->{condensed}) {
4840 warn "$me _items_pkg condensing section\n"
4844 local $Storable::canonical = 1;
4845 foreach ( @items ) {
4847 delete $item->{ref};
4848 delete $item->{ext_description};
4849 my $key = freeze($item);
4850 $itemshash{$key} ||= 0;
4851 $itemshash{$key} ++; # += $item->{quantity};
4853 @items = sort { $a->{description} cmp $b->{description} }
4854 map { my $i = thaw($_);
4855 $i->{quantity} = $itemshash{$_};
4857 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4863 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4870 return 0 unless $a->itemdesc cmp $b->itemdesc;
4871 return -1 if $b->itemdesc eq 'Tax';
4872 return 1 if $a->itemdesc eq 'Tax';
4873 return -1 if $b->itemdesc eq 'Other surcharges';
4874 return 1 if $a->itemdesc eq 'Other surcharges';
4875 $a->itemdesc cmp $b->itemdesc;
4880 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4881 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4884 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4886 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4887 list of hashrefs describing the line items they generate on the invoice.
4889 OPTIONS may include:
4891 format: the invoice format.
4893 escape_function: the function used to escape strings.
4895 DEPRECATED? (expensive, mostly unused?)
4896 format_function: the function used to format CDRs.
4898 section: a hashref containing 'description'; if this is present,
4899 cust_bill_pkg_display records not belonging to this section are
4902 multisection: a flag indicating that this is a multisection invoice,
4903 which does something complicated.
4905 multilocation: a flag to display the location label for the package.
4907 Returns a list of hashrefs, each of which may contain:
4909 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4910 ext_description, which is an arrayref of detail lines to show below
4915 sub _items_cust_bill_pkg {
4917 my $conf = $self->conf;
4918 my $cust_bill_pkgs = shift;
4921 my $format = $opt{format} || '';
4922 my $escape_function = $opt{escape_function} || sub { shift };
4923 my $format_function = $opt{format_function} || '';
4924 my $no_usage = $opt{no_usage} || '';
4925 my $unsquelched = $opt{unsquelched} || ''; #unused
4926 my $section = $opt{section}->{description} if $opt{section};
4927 my $summary_page = $opt{summary_page} || ''; #unused
4928 my $multilocation = $opt{multilocation} || '';
4929 my $multisection = $opt{multisection} || '';
4930 my $discount_show_always = 0;
4932 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4934 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4937 my ($s, $r, $u) = ( undef, undef, undef );
4938 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4941 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4942 if ( $_ && !$cust_bill_pkg->hidden ) {
4943 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4944 $_->{amount} =~ s/^\-0\.00$/0.00/;
4945 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4947 if $_->{amount} != 0
4948 || $discount_show_always
4949 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4950 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4956 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4958 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4959 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4962 foreach my $display ( grep { defined($section)
4963 ? $_->section eq $section
4966 #grep { !$_->summary || !$summary_page } # bunk!
4967 grep { !$_->summary || $multisection }
4968 @cust_bill_pkg_display
4972 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4973 $display->billpkgdisplaynum. "\n"
4976 my $type = $display->type;
4978 my $desc = $cust_bill_pkg->desc;
4979 $desc = substr($desc, 0, $maxlength). '...'
4980 if $format eq 'latex' && length($desc) > $maxlength;
4982 my %details_opt = ( 'format' => $format,
4983 'escape_function' => $escape_function,
4984 'format_function' => $format_function,
4985 'no_usage' => $opt{'no_usage'},
4988 if ( $cust_bill_pkg->pkgnum > 0 ) {
4990 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4993 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4995 # which pkgpart to show for display purposes?
4996 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
4998 # start/end dates for invoice formats that do nonstandard
5000 my %item_dates = ();
5001 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5002 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5004 if ( (!$type || $type eq 'S')
5005 && ( $cust_bill_pkg->setup != 0
5006 || $cust_bill_pkg->setup_show_zero
5011 warn "$me _items_cust_bill_pkg adding setup\n"
5014 my $description = $desc;
5015 $description .= ' Setup'
5016 if $cust_bill_pkg->recur != 0
5017 || $discount_show_always
5018 || $cust_bill_pkg->recur_show_zero;
5021 unless ( $cust_pkg->part_pkg->hide_svc_detail
5022 || $cust_bill_pkg->hidden )
5025 push @d, map &{$escape_function}($_),
5026 $cust_pkg->h_labels_short($self->_date, undef, 'I')
5027 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5029 if ( $multilocation ) {
5030 my $loc = $cust_pkg->location_label;
5031 $loc = substr($loc, 0, $maxlength). '...'
5032 if $format eq 'latex' && length($loc) > $maxlength;
5033 push @d, &{$escape_function}($loc);
5036 } #unless hiding service details
5038 push @d, $cust_bill_pkg->details(%details_opt)
5039 if $cust_bill_pkg->recur == 0;
5041 if ( $cust_bill_pkg->hidden ) {
5042 $s->{amount} += $cust_bill_pkg->setup;
5043 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5044 push @{ $s->{ext_description} }, @d;
5048 description => $description,
5049 pkgpart => $pkgpart,
5050 pkgnum => $cust_bill_pkg->pkgnum,
5051 amount => $cust_bill_pkg->setup,
5052 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5053 unit_amount => $cust_bill_pkg->unitsetup,
5054 quantity => $cust_bill_pkg->quantity,
5055 ext_description => \@d,
5061 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5063 $cust_bill_pkg->recur != 0
5064 || $cust_bill_pkg->setup == 0
5065 || $discount_show_always
5066 || $cust_bill_pkg->recur_show_zero
5071 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5074 my $is_summary = $display->summary;
5075 my $description = ($is_summary && $type && $type eq 'U')
5076 ? "Usage charges" : $desc;
5078 my $part_pkg = $cust_pkg->part_pkg;
5080 #pry be a bit more efficient to look some of this conf stuff up
5083 $conf->exists('disable_line_item_date_ranges')
5084 || $part_pkg->option('disable_line_item_date_ranges',1)
5087 my $date_style = '';
5088 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5089 $cust_main->agentnum
5091 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5092 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5093 $cust_main->agentnum
5095 if ( defined($date_style) && $date_style eq 'month_of' ) {
5096 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5097 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5098 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5099 $cust_main->agentnum
5101 $desc .= ' ' unless $desc =~ /\s$/;
5102 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5104 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5105 " - ". time2str($date_format, $cust_bill_pkg->edate);
5107 $description .= " ($time_period)";
5111 my @seconds = (); # for display of usage info
5113 #at least until cust_bill_pkg has "past" ranges in addition to
5114 #the "future" sdate/edate ones... see #3032
5115 my @dates = ( $self->_date );
5116 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5117 push @dates, $prev->sdate if $prev;
5118 push @dates, undef if !$prev;
5120 unless ( $cust_pkg->part_pkg->hide_svc_detail
5121 || $cust_bill_pkg->itemdesc
5122 || $cust_bill_pkg->hidden
5123 || $is_summary && $type && $type eq 'U' )
5126 warn "$me _items_cust_bill_pkg adding service details\n"
5129 push @d, map &{$escape_function}($_),
5130 $cust_pkg->h_labels_short(@dates, 'I')
5131 #$cust_bill_pkg->edate,
5132 #$cust_bill_pkg->sdate)
5133 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5135 warn "$me _items_cust_bill_pkg done adding service details\n"
5138 if ( $multilocation ) {
5139 my $loc = $cust_pkg->location_label;
5140 $loc = substr($loc, 0, $maxlength). '...'
5141 if $format eq 'latex' && length($loc) > $maxlength;
5142 push @d, &{$escape_function}($loc);
5145 # Display of seconds_since_sqlradacct:
5146 # On the invoice, when processing @detail_items, look for a field
5147 # named 'seconds'. This will contain total seconds for each
5148 # service, in the same order as @ext_description. For services
5149 # that don't support this it will show undef.
5150 if ( $conf->exists('svc_acct-usage_seconds')
5151 and ! $cust_bill_pkg->pkgpart_override ) {
5152 foreach my $cust_svc (
5153 $cust_pkg->h_cust_svc(@dates, 'I')
5156 # eval because not having any part_export_usage exports
5157 # is a fatal error, last_bill/_date because that's how
5158 # sqlradius_hour billing does it
5160 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5162 push @seconds, $sec;
5164 } #if svc_acct-usage_seconds
5168 unless ( $is_summary ) {
5169 warn "$me _items_cust_bill_pkg adding details\n"
5172 #instead of omitting details entirely in this case (unwanted side
5173 # effects), just omit CDRs
5174 $details_opt{'no_usage'} = 1
5175 if $type && $type eq 'R';
5177 push @d, $cust_bill_pkg->details(%details_opt);
5180 warn "$me _items_cust_bill_pkg calculating amount\n"
5185 $amount = $cust_bill_pkg->recur;
5186 } elsif ($type eq 'R') {
5187 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5188 } elsif ($type eq 'U') {
5189 $amount = $cust_bill_pkg->usage;
5192 if ( !$type || $type eq 'R' ) {
5194 warn "$me _items_cust_bill_pkg adding recur\n"
5197 if ( $cust_bill_pkg->hidden ) {
5198 $r->{amount} += $amount;
5199 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5200 push @{ $r->{ext_description} }, @d;
5203 description => $description,
5204 pkgpart => $pkgpart,
5205 pkgnum => $cust_bill_pkg->pkgnum,
5207 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5208 unit_amount => $cust_bill_pkg->unitrecur,
5209 quantity => $cust_bill_pkg->quantity,
5211 ext_description => \@d,
5213 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5216 } else { # $type eq 'U'
5218 warn "$me _items_cust_bill_pkg adding usage\n"
5221 if ( $cust_bill_pkg->hidden ) {
5222 $u->{amount} += $amount;
5223 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5224 push @{ $u->{ext_description} }, @d;
5227 description => $description,
5228 pkgpart => $pkgpart,
5229 pkgnum => $cust_bill_pkg->pkgnum,
5231 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5232 unit_amount => $cust_bill_pkg->unitrecur,
5233 quantity => $cust_bill_pkg->quantity,
5235 ext_description => \@d,
5240 } # recurring or usage with recurring charge
5242 } else { #pkgnum tax or one-shot line item (??)
5244 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5247 if ( $cust_bill_pkg->setup != 0 ) {
5249 'description' => $desc,
5250 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5253 if ( $cust_bill_pkg->recur != 0 ) {
5255 'description' => "$desc (".
5256 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5257 time2str($date_format, $cust_bill_pkg->edate). ')',
5258 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5266 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5267 && $conf->exists('discount-show-always'));
5271 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5273 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5274 $_->{amount} =~ s/^\-0\.00$/0.00/;
5275 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5277 if $_->{amount} != 0
5278 || $discount_show_always
5279 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5280 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5284 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5291 sub _items_credits {
5292 my( $self, %opt ) = @_;
5293 my $trim_len = $opt{'trim_len'} || 60;
5297 foreach ( $self->cust_credited ) {
5299 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5301 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5302 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5303 $reason = " ($reason) " if $reason;
5306 #'description' => 'Credit ref\#'. $_->crednum.
5307 # " (". time2str("%x",$_->cust_credit->_date) .")".
5309 'description' => $self->mt('Credit applied').' '.
5310 time2str($date_format,$_->cust_credit->_date). $reason,
5311 'amount' => sprintf("%.2f",$_->amount),
5319 sub _items_payments {
5323 #get & print payments
5324 foreach ( $self->cust_bill_pay ) {
5326 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5329 'description' => $self->mt('Payment received').' '.
5330 time2str($date_format,$_->cust_pay->_date ),
5331 'amount' => sprintf("%.2f", $_->amount )
5339 =item _items_discounts_avail
5341 Returns an array of line item hashrefs representing available term discounts
5342 for this invoice. This makes the same assumptions that apply to term
5343 discounts in general: that the package is billed monthly, at a flat rate,
5344 with no usage charges. A prorated first month will be handled, as will
5345 a setup fee if the discount is allowed to apply to setup fees.
5349 sub _items_discounts_avail {
5351 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5353 my %plans = $self->discount_plans;
5355 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5359 my $plan = $plans{$months};
5361 my $term_total = sprintf('%.2f', $plan->discounted_total);
5362 my $percent = sprintf('%.0f',
5363 100 * (1 - $term_total / $plan->base_total) );
5364 my $permonth = sprintf('%.2f', $term_total / $months);
5365 my $detail = $self->mt('discount on item'). ' '.
5366 join(', ', map { "#$_" } $plan->pkgnums)
5369 # discounts for non-integer months don't work anyway
5370 $months = sprintf("%d", $months);
5373 description => $self->mt('Save [_1]% by paying for [_2] months',
5375 amount => $self->mt('[_1] ([_2] per month)',
5376 $term_total, $money_char.$permonth),
5377 ext_description => ($detail || ''),
5380 sort { $b <=> $a } keys %plans;
5384 =item call_details [ OPTION => VALUE ... ]
5386 Returns an array of CSV strings representing the call details for this invoice
5387 The only option available is the boolean prepend_billed_number
5392 my ($self, %opt) = @_;
5394 my $format_function = sub { shift };
5396 if ($opt{prepend_billed_number}) {
5397 $format_function = sub {
5401 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5406 my @details = map { $_->details( 'format_function' => $format_function,
5407 'escape_function' => sub{ return() },
5411 $self->cust_bill_pkg;
5412 my $header = $details[0];
5413 ( $header, grep { $_ ne $header } @details );
5423 =item process_reprint
5427 sub process_reprint {
5428 process_re_X('print', @_);
5431 =item process_reemail
5435 sub process_reemail {
5436 process_re_X('email', @_);
5444 process_re_X('fax', @_);
5452 process_re_X('ftp', @_);
5459 sub process_respool {
5460 process_re_X('spool', @_);
5463 use Storable qw(thaw);
5467 my( $method, $job ) = ( shift, shift );
5468 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5470 my $param = thaw(decode_base64(shift));
5471 warn Dumper($param) if $DEBUG;
5482 my($method, $job, %param ) = @_;
5484 warn "re_X $method for job $job with param:\n".
5485 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5488 #some false laziness w/search/cust_bill.html
5490 my $orderby = 'ORDER BY cust_bill._date';
5492 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5494 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5496 my @cust_bill = qsearch( {
5497 #'select' => "cust_bill.*",
5498 'table' => 'cust_bill',
5499 'addl_from' => $addl_from,
5501 'extra_sql' => $extra_sql,
5502 'order_by' => $orderby,
5506 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5508 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5511 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5512 foreach my $cust_bill ( @cust_bill ) {
5513 $cust_bill->$method();
5515 if ( $job ) { #progressbar foo
5517 if ( time - $min_sec > $last ) {
5518 my $error = $job->update_statustext(
5519 int( 100 * $num / scalar(@cust_bill) )
5521 die $error if $error;
5532 =head1 CLASS METHODS
5538 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5543 my ($class, $start, $end) = @_;
5545 $class->paid_sql($start, $end). ' - '.
5546 $class->credited_sql($start, $end);
5551 Returns an SQL fragment to retreive the net amount (charged minus credited).
5556 my ($class, $start, $end) = @_;
5557 'charged - '. $class->credited_sql($start, $end);
5562 Returns an SQL fragment to retreive the amount paid against this invoice.
5567 my ($class, $start, $end) = @_;
5568 $start &&= "AND cust_bill_pay._date <= $start";
5569 $end &&= "AND cust_bill_pay._date > $end";
5570 $start = '' unless defined($start);
5571 $end = '' unless defined($end);
5572 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5573 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5578 Returns an SQL fragment to retreive the amount credited against this invoice.
5583 my ($class, $start, $end) = @_;
5584 $start &&= "AND cust_credit_bill._date <= $start";
5585 $end &&= "AND cust_credit_bill._date > $end";
5586 $start = '' unless defined($start);
5587 $end = '' unless defined($end);
5588 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5589 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5594 Returns an SQL fragment to retrieve the due date of an invoice.
5595 Currently only supported on PostgreSQL.
5600 my $conf = new FS::Conf;
5604 cust_bill.invoice_terms,
5605 cust_main.invoice_terms,
5606 \''.($conf->config('invoice_default_terms') || '').'\'
5607 ), E\'Net (\\\\d+)\'
5609 ) * 86400 + cust_bill._date'
5612 =item search_sql_where HASHREF
5614 Class method which returns an SQL WHERE fragment to search for parameters
5615 specified in HASHREF. Valid parameters are
5621 List reference of start date, end date, as UNIX timestamps.
5631 List reference of charged limits (exclusive).
5635 List reference of charged limits (exclusive).
5639 flag, return open invoices only
5643 flag, return net invoices only
5647 =item newest_percust
5651 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5655 sub search_sql_where {
5656 my($class, $param) = @_;
5658 warn "$me search_sql_where called with params: \n".
5659 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5665 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5666 push @search, "cust_main.agentnum = $1";
5670 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5671 push @search, "cust_main.refnum = $1";
5675 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5676 push @search, "cust_bill.custnum = $1";
5680 if ( $param->{'cust_classnum'} ) {
5681 my $classnums = $param->{'cust_classnum'};
5682 $classnums = [ $classnums ] if !ref($classnums);
5683 $classnums = [ grep /^\d+$/, @$classnums ];
5684 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5689 if ( $param->{_date} ) {
5690 my($beginning, $ending) = @{$param->{_date}};
5692 push @search, "cust_bill._date >= $beginning",
5693 "cust_bill._date < $ending";
5697 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5698 push @search, "cust_bill.invnum >= $1";
5700 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5701 push @search, "cust_bill.invnum <= $1";
5705 if ( $param->{charged} ) {
5706 my @charged = ref($param->{charged})
5707 ? @{ $param->{charged} }
5708 : ($param->{charged});
5710 push @search, map { s/^charged/cust_bill.charged/; $_; }
5714 my $owed_sql = FS::cust_bill->owed_sql;
5717 if ( $param->{owed} ) {
5718 my @owed = ref($param->{owed})
5719 ? @{ $param->{owed} }
5721 push @search, map { s/^owed/$owed_sql/; $_; }
5726 push @search, "0 != $owed_sql"
5727 if $param->{'open'};
5728 push @search, '0 != '. FS::cust_bill->net_sql
5732 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5733 if $param->{'days'};
5736 if ( $param->{'newest_percust'} ) {
5738 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5739 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5741 my @newest_where = map { my $x = $_;
5742 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5745 grep ! /^cust_main./, @search;
5746 my $newest_where = scalar(@newest_where)
5747 ? ' AND '. join(' AND ', @newest_where)
5751 push @search, "cust_bill._date = (
5752 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5753 WHERE newest_cust_bill.custnum = cust_bill.custnum
5759 #promised_date - also has an option to accept nulls
5760 if ( $param->{promised_date} ) {
5761 my($beginning, $ending, $null) = @{$param->{promised_date}};
5763 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5764 "cust_bill.promised_date < $ending )" .
5765 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5768 #agent virtualization
5769 my $curuser = $FS::CurrentUser::CurrentUser;
5770 if ( $curuser->username eq 'fs_queue'
5771 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5773 my $newuser = qsearchs('access_user', {
5774 'username' => $username,
5778 $curuser = $newuser;
5780 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5783 push @search, $curuser->agentnums_sql;
5785 join(' AND ', @search );
5797 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5798 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base