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->{'quantity'} = 1;
3074 $detail->{'section'} = $multisection ? $previous_section
3076 $detail->{'description'} = &$escape_function($line_item->{'description'});
3077 if ( exists $line_item->{'ext_description'} ) {
3078 @{$detail->{'ext_description'}} = map {
3079 &$escape_function($_);
3080 } @{$line_item->{'ext_description'}};
3082 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3083 $line_item->{'amount'};
3084 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3086 push @detail_items, $detail;
3087 push @buf, [ $detail->{'description'},
3088 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3094 if ( @pr_cust_bill && $self->enable_previous ) {
3095 push @buf, ['','-----------'];
3096 push @buf, [ $self->mt('Total Previous Balance'),
3097 $money_char. sprintf("%10.2f", $pr_total) ];
3101 if ( $conf->exists('svc_phone-did-summary') ) {
3102 warn "$me adding DID summary\n"
3105 my ($didsummary,$minutes) = $self->_did_summary;
3106 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3108 { 'description' => $didsummary_desc,
3109 'ext_description' => [ $didsummary, $minutes ],
3113 foreach my $section (@sections, @$late_sections) {
3115 warn "$me adding section \n". Dumper($section)
3118 # begin some normalization
3119 $section->{'subtotal'} = $section->{'amount'}
3121 && !exists($section->{subtotal})
3122 && exists($section->{amount});
3124 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3125 if ( $invoice_data{finance_section} &&
3126 $section->{'description'} eq $invoice_data{finance_section} );
3128 $section->{'subtotal'} = $other_money_char.
3129 sprintf('%.2f', $section->{'subtotal'})
3132 # continue some normalization
3133 $section->{'amount'} = $section->{'subtotal'}
3137 if ( $section->{'description'} ) {
3138 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3143 warn "$me setting options\n"
3146 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3148 $options{'section'} = $section if $multisection;
3149 $options{'format'} = $format;
3150 $options{'escape_function'} = $escape_function;
3151 $options{'no_usage'} = 1 unless $unsquelched;
3152 $options{'unsquelched'} = $unsquelched;
3153 $options{'summary_page'} = $summarypage;
3154 $options{'skip_usage'} =
3155 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3156 $options{'multilocation'} = $multilocation;
3157 $options{'multisection'} = $multisection;
3159 warn "$me searching for line items\n"
3162 foreach my $line_item ( $self->_items_pkg(%options) ) {
3164 warn "$me adding line item $line_item\n"
3168 ext_description => [],
3170 $detail->{'ref'} = $line_item->{'pkgnum'};
3171 $detail->{'quantity'} = $line_item->{'quantity'};
3172 $detail->{'section'} = $section;
3173 $detail->{'description'} = &$escape_function($line_item->{'description'});
3174 if ( exists $line_item->{'ext_description'} ) {
3175 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3177 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3178 $line_item->{'amount'};
3179 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3180 $line_item->{'unit_amount'};
3181 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3183 $detail->{'sdate'} = $line_item->{'sdate'};
3184 $detail->{'edate'} = $line_item->{'edate'};
3185 $detail->{'seconds'} = $line_item->{'seconds'};
3187 push @detail_items, $detail;
3188 push @buf, ( [ $detail->{'description'},
3189 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3191 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3195 if ( $section->{'description'} ) {
3196 push @buf, ( ['','-----------'],
3197 [ $section->{'description'}. ' sub-total',
3198 $section->{'subtotal'} # already formatted this
3207 $invoice_data{current_less_finance} =
3208 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3210 # create a major section for previous balance if we have major sections,
3211 # or if previous_section is in summary form
3212 if ( ( $multisection && $self->enable_previous )
3213 || $conf->exists('previous_balance-summary_only') )
3215 unshift @sections, $previous_section if $pr_total;
3218 warn "$me adding taxes\n"
3221 foreach my $tax ( $self->_items_tax ) {
3223 $taxtotal += $tax->{'amount'};
3225 my $description = &$escape_function( $tax->{'description'} );
3226 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3228 if ( $multisection ) {
3230 my $money = $old_latex ? '' : $money_char;
3231 push @detail_items, {
3232 ext_description => [],
3235 description => $description,
3236 amount => $money. $amount,
3238 section => $tax_section,
3243 push @total_items, {
3244 'total_item' => $description,
3245 'total_amount' => $other_money_char. $amount,
3250 push @buf,[ $description,
3251 $money_char. $amount,
3258 $total->{'total_item'} = $self->mt('Sub-total');
3259 $total->{'total_amount'} =
3260 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3262 if ( $multisection ) {
3263 $tax_section->{'subtotal'} = $other_money_char.
3264 sprintf('%.2f', $taxtotal);
3265 $tax_section->{'pretotal'} = 'New charges sub-total '.
3266 $total->{'total_amount'};
3267 push @sections, $tax_section if $taxtotal;
3269 unshift @total_items, $total;
3272 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3274 push @buf,['','-----------'];
3275 push @buf,[$self->mt(
3276 (!$self->enable_previous)
3278 : 'Total New Charges'
3280 $money_char. sprintf("%10.2f",$self->charged) ];
3283 # calculate total, possibly including total owed on previous
3288 $item = $conf->config('previous_balance-exclude_from_total')
3289 || 'Total New Charges'
3290 if $conf->exists('previous_balance-exclude_from_total');
3291 my $amount = $self->charged;
3292 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3293 $amount += $pr_total;
3296 $total->{'total_item'} = &$embolden_function($self->mt($item));
3297 $total->{'total_amount'} =
3298 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3299 if ( $multisection ) {
3300 if ( $adjust_section->{'sort_weight'} ) {
3301 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3302 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3304 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3305 $other_money_char. sprintf('%.2f', $self->charged );
3308 push @total_items, $total;
3310 push @buf,['','-----------'];
3313 sprintf( '%10.2f', $amount )
3318 # if we're showing previous invoices, also show previous
3319 # credits and payments
3320 if ( $self->enable_previous
3321 and $self->can('_items_credits')
3322 and $self->can('_items_payments') )
3324 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3327 my $credittotal = 0;
3328 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3331 $total->{'total_item'} = &$escape_function($credit->{'description'});
3332 $credittotal += $credit->{'amount'};
3333 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3334 $adjusttotal += $credit->{'amount'};
3335 if ( $multisection ) {
3336 my $money = $old_latex ? '' : $money_char;
3337 push @detail_items, {
3338 ext_description => [],
3341 description => &$escape_function($credit->{'description'}),
3342 amount => $money. $credit->{'amount'},
3344 section => $adjust_section,
3347 push @total_items, $total;
3351 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3354 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3355 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3359 my $paymenttotal = 0;
3360 foreach my $payment ( $self->_items_payments ) {
3362 $total->{'total_item'} = &$escape_function($payment->{'description'});
3363 $paymenttotal += $payment->{'amount'};
3364 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3365 $adjusttotal += $payment->{'amount'};
3366 if ( $multisection ) {
3367 my $money = $old_latex ? '' : $money_char;
3368 push @detail_items, {
3369 ext_description => [],
3372 description => &$escape_function($payment->{'description'}),
3373 amount => $money. $payment->{'amount'},
3375 section => $adjust_section,
3378 push @total_items, $total;
3380 push @buf, [ $payment->{'description'},
3381 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3384 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3386 if ( $multisection ) {
3387 $adjust_section->{'subtotal'} = $other_money_char.
3388 sprintf('%.2f', $adjusttotal);
3389 push @sections, $adjust_section
3390 unless $adjust_section->{sort_weight};
3393 # create Balance Due message
3396 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3397 $total->{'total_amount'} =
3398 &$embolden_function(
3399 $other_money_char. sprintf('%.2f', $summarypage
3401 $self->billing_balance
3402 : $self->owed + $pr_total
3405 if ( $multisection && !$adjust_section->{sort_weight} ) {
3406 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3407 $total->{'total_amount'};
3409 push @total_items, $total;
3411 push @buf,['','-----------'];
3412 push @buf,[$self->balance_due_msg, $money_char.
3413 sprintf("%10.2f", $balance_due ) ];
3416 if ( $conf->exists('previous_balance-show_credit')
3417 and $cust_main->balance < 0 ) {
3418 my $credit_total = {
3419 'total_item' => &$embolden_function($self->credit_balance_msg),
3420 'total_amount' => &$embolden_function(
3421 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3424 if ( $multisection ) {
3425 $adjust_section->{'posttotal'} .= $newline_token .
3426 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3429 push @total_items, $credit_total;
3431 push @buf,['','-----------'];
3432 push @buf,[$self->credit_balance_msg, $money_char.
3433 sprintf("%10.2f", -$cust_main->balance ) ];
3437 if ( $multisection ) {
3438 if ($conf->exists('svc_phone_sections')) {
3440 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3441 $total->{'total_amount'} =
3442 &$embolden_function(
3443 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3445 my $last_section = pop @sections;
3446 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3447 $total->{'total_amount'};
3448 push @sections, $last_section;
3450 push @sections, @$late_sections
3454 # make a discounts-available section, even without multisection
3455 if ( $conf->exists('discount-show_available')
3456 and my @discounts_avail = $self->_items_discounts_avail ) {
3457 my $discount_section = {
3458 'description' => $self->mt('Discounts Available'),
3463 push @sections, $discount_section;
3464 push @detail_items, map { +{
3465 'ref' => '', #should this be something else?
3466 'section' => $discount_section,
3467 'description' => &$escape_function( $_->{description} ),
3468 'amount' => $money_char . &$escape_function( $_->{amount} ),
3469 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3470 } } @discounts_avail;
3473 # All sections and items are built; now fill in templates.
3474 my @includelist = ();
3475 push @includelist, 'summary' if $summarypage;
3476 foreach my $include ( @includelist ) {
3478 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3481 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3483 @inc_src = $conf->config($inc_file, $agentnum);
3487 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3489 my $convert_map = $convert_maps{$format}{$include};
3491 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3492 s/--\@\]/$delimiters{$format}[1]/g;
3495 &$convert_map( $conf->config($inc_file, $agentnum) );
3499 my $inc_tt = new Text::Template (
3501 SOURCE => [ map "$_\n", @inc_src ],
3502 DELIMITERS => $delimiters{$format},
3503 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3505 unless ( $inc_tt->compile() ) {
3506 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3507 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3511 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3513 $invoice_data{$include} =~ s/\n+$//
3514 if ($format eq 'latex');
3519 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3520 /invoice_lines\((\d*)\)/;
3521 $invoice_lines += $1 || scalar(@buf);
3524 die "no invoice_lines() functions in template?"
3525 if ( $format eq 'template' && !$wasfunc );
3527 if ($format eq 'template') {
3529 if ( $invoice_lines ) {
3530 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3531 $invoice_data{'total_pages'}++
3532 if scalar(@buf) % $invoice_lines;
3535 #setup subroutine for the template
3536 $invoice_data{invoice_lines} = sub {
3537 my $lines = shift || scalar(@buf);
3549 push @collect, split("\n",
3550 $text_template->fill_in( HASH => \%invoice_data )
3552 $invoice_data{'page'}++;
3554 map "$_\n", @collect;
3556 # this is where we actually create the invoice
3557 warn "filling in template for invoice ". $self->invnum. "\n"
3559 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3562 $text_template->fill_in(HASH => \%invoice_data);
3566 # helper routine for generating date ranges
3567 sub _prior_month30s {
3570 [ 1, 2592000 ], # 0-30 days ago
3571 [ 2592000, 5184000 ], # 30-60 days ago
3572 [ 5184000, 7776000 ], # 60-90 days ago
3573 [ 7776000, 0 ], # 90+ days ago
3576 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3577 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3582 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3584 Returns an postscript invoice, as a scalar.
3586 Options can be passed as a hashref (recommended) or as a list of time, template
3587 and then any key/value pairs for any other options.
3589 I<time> an optional value used to control the printing of overdue messages. The
3590 default is now. It isn't the date of the invoice; that's the `_date' field.
3591 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3592 L<Time::Local> and L<Date::Parse> for conversion functions.
3594 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3601 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3602 my $ps = generate_ps($file);
3604 unlink($barcodefile) if $barcodefile;
3609 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3611 Returns an PDF invoice, as a scalar.
3613 Options can be passed as a hashref (recommended) or as a list of time, template
3614 and then any key/value pairs for any other options.
3616 I<time> an optional value used to control the printing of overdue messages. The
3617 default is now. It isn't the date of the invoice; that's the `_date' field.
3618 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3619 L<Time::Local> and L<Date::Parse> for conversion functions.
3621 I<template>, if specified, is the name of a suffix for alternate invoices.
3623 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3630 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3631 my $pdf = generate_pdf($file);
3633 unlink($barcodefile) if $barcodefile;
3638 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3640 Returns an HTML invoice, as a scalar.
3642 I<time> an optional value used to control the printing of overdue messages. The
3643 default is now. It isn't the date of the invoice; that's the `_date' field.
3644 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3645 L<Time::Local> and L<Date::Parse> for conversion functions.
3647 I<template>, if specified, is the name of a suffix for alternate invoices.
3649 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3651 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3652 when emailing the invoice as part of a multipart/related MIME email.
3660 %params = %{ shift() };
3662 $params{'time'} = shift;
3663 $params{'template'} = shift;
3664 $params{'cid'} = shift;
3667 $params{'format'} = 'html';
3669 $self->print_generic( %params );
3672 # quick subroutine for print_latex
3674 # There are ten characters that LaTeX treats as special characters, which
3675 # means that they do not simply typeset themselves:
3676 # # $ % & ~ _ ^ \ { }
3678 # TeX ignores blanks following an escaped character; if you want a blank (as
3679 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3683 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3684 $value =~ s/([<>])/\$$1\$/g;
3690 encode_entities($value);
3694 sub _html_escape_nbsp {
3695 my $value = _html_escape(shift);
3696 $value =~ s/ +/ /g;
3700 #utility methods for print_*
3702 sub _translate_old_latex_format {
3703 warn "_translate_old_latex_format called\n"
3710 if ( $line =~ /^%%Detail\s*$/ ) {
3712 push @template, q![@--!,
3713 q! foreach my $_tr_line (@detail_items) {!,
3714 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3715 q! $_tr_line->{'description'} .= !,
3716 q! "\\tabularnewline\n~~".!,
3717 q! join( "\\tabularnewline\n~~",!,
3718 q! @{$_tr_line->{'ext_description'}}!,
3722 while ( ( my $line_item_line = shift )
3723 !~ /^%%EndDetail\s*$/ ) {
3724 $line_item_line =~ s/'/\\'/g; # nice LTS
3725 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3726 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3727 push @template, " \$OUT .= '$line_item_line';";
3730 push @template, '}',
3733 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3735 push @template, '[@--',
3736 ' foreach my $_tr_line (@total_items) {';
3738 while ( ( my $total_item_line = shift )
3739 !~ /^%%EndTotalDetails\s*$/ ) {
3740 $total_item_line =~ s/'/\\'/g; # nice LTS
3741 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3742 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3743 push @template, " \$OUT .= '$total_item_line';";
3746 push @template, '}',
3750 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3751 push @template, $line;
3757 warn "$_\n" foreach @template;
3765 my $conf = $self->conf;
3767 #check for an invoice-specific override
3768 return $self->invoice_terms if $self->invoice_terms;
3770 #check for a customer- specific override
3771 my $cust_main = $self->cust_main;
3772 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3774 #use configured default
3775 $conf->config('invoice_default_terms') || '';
3781 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3782 $duedate = $self->_date() + ( $1 * 86400 );
3789 $self->due_date ? time2str(shift, $self->due_date) : '';
3792 sub balance_due_msg {
3794 my $msg = $self->mt('Balance Due');
3795 return $msg unless $self->terms;
3796 if ( $self->due_date ) {
3797 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3798 $self->due_date2str($date_format);
3799 } elsif ( $self->terms ) {
3800 $msg .= ' - '. $self->terms;
3805 sub balance_due_date {
3807 my $conf = $self->conf;
3809 if ( $conf->exists('invoice_default_terms')
3810 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3811 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3816 sub credit_balance_msg {
3818 $self->mt('Credit Balance Remaining')
3821 =item invnum_date_pretty
3823 Returns a string with the invoice number and date, for example:
3824 "Invoice #54 (3/20/2008)"
3828 sub invnum_date_pretty {
3830 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3835 Returns a string with the date, for example: "3/20/2008"
3841 time2str($date_format, $self->_date);
3844 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3846 Generate section information for all items appearing on this invoice.
3847 This will only be called for multi-section invoices.
3849 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3850 related display records (L<FS::cust_bill_pkg_display>) and organize
3851 them into two groups ("early" and "late" according to whether they come
3852 before or after the total), then into sections. A subtotal is calculated
3855 Section descriptions are returned in sort weight order. Each consists
3856 of a hash containing:
3858 description: the package category name, escaped
3859 subtotal: the total charges in that section
3860 tax_section: a flag indicating that the section contains only tax charges
3861 summarized: same as tax_section, for some reason
3862 sort_weight: the package category's sort weight
3864 If 'condense' is set on the display record, it also contains everything
3865 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3866 coderefs to generate parts of the invoice. This is not advised.
3870 LATE: an arrayref to push the "late" section hashes onto. The "early"
3871 group is simply returned from the method.
3873 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3874 Turning this on has the following effects:
3875 - Ignores display items with the 'summary' flag.
3876 - Combines all items into the "early" group.
3877 - Creates sections for all non-disabled package categories, even if they
3878 have no charges on this invoice, as well as a section with no name.
3880 ESCAPE: an escape function to use for section titles.
3882 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3883 sorted list. If there are any of these, section subtotals exclude
3886 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3887 passed through to C<_condense_section()>.
3891 use vars qw(%pkg_category_cache);
3892 sub _items_sections {
3895 my $summarypage = shift;
3897 my $extra_sections = shift;
3901 my %late_subtotal = ();
3904 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3907 my $usage = $cust_bill_pkg->usage;
3909 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3910 next if ( $display->summary && $summarypage );
3912 my $section = $display->section;
3913 my $type = $display->type;
3915 $not_tax{$section} = 1
3916 unless $cust_bill_pkg->pkgnum == 0;
3918 if ( $display->post_total && !$summarypage ) {
3919 if (! $type || $type eq 'S') {
3920 $late_subtotal{$section} += $cust_bill_pkg->setup
3921 if $cust_bill_pkg->setup != 0
3922 || $cust_bill_pkg->setup_show_zero;
3926 $late_subtotal{$section} += $cust_bill_pkg->recur
3927 if $cust_bill_pkg->recur != 0
3928 || $cust_bill_pkg->recur_show_zero;
3931 if ($type && $type eq 'R') {
3932 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3933 if $cust_bill_pkg->recur != 0
3934 || $cust_bill_pkg->recur_show_zero;
3937 if ($type && $type eq 'U') {
3938 $late_subtotal{$section} += $usage
3939 unless scalar(@$extra_sections);
3944 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3946 if (! $type || $type eq 'S') {
3947 $subtotal{$section} += $cust_bill_pkg->setup
3948 if $cust_bill_pkg->setup != 0
3949 || $cust_bill_pkg->setup_show_zero;
3953 $subtotal{$section} += $cust_bill_pkg->recur
3954 if $cust_bill_pkg->recur != 0
3955 || $cust_bill_pkg->recur_show_zero;
3958 if ($type && $type eq 'R') {
3959 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3960 if $cust_bill_pkg->recur != 0
3961 || $cust_bill_pkg->recur_show_zero;
3964 if ($type && $type eq 'U') {
3965 $subtotal{$section} += $usage
3966 unless scalar(@$extra_sections);
3975 %pkg_category_cache = ();
3977 push @$late, map { { 'description' => &{$escape}($_),
3978 'subtotal' => $late_subtotal{$_},
3980 'sort_weight' => ( _pkg_category($_)
3981 ? _pkg_category($_)->weight
3984 ((_pkg_category($_) && _pkg_category($_)->condense)
3985 ? $self->_condense_section($format)
3989 sort _sectionsort keys %late_subtotal;
3992 if ( $summarypage ) {
3993 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3994 map { $_->categoryname } qsearch('pkg_category', {});
3995 push @sections, '' if exists($subtotal{''});
3997 @sections = keys %subtotal;
4000 my @early = map { { 'description' => &{$escape}($_),
4001 'subtotal' => $subtotal{$_},
4002 'summarized' => $not_tax{$_} ? '' : 'Y',
4003 'tax_section' => $not_tax{$_} ? '' : 'Y',
4004 'sort_weight' => ( _pkg_category($_)
4005 ? _pkg_category($_)->weight
4008 ((_pkg_category($_) && _pkg_category($_)->condense)
4009 ? $self->_condense_section($format)
4014 push @early, @$extra_sections if $extra_sections;
4016 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4020 #helper subs for above
4023 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4027 my $categoryname = shift;
4028 $pkg_category_cache{$categoryname} ||=
4029 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4032 my %condensed_format = (
4033 'label' => [ qw( Description Qty Amount ) ],
4035 sub { shift->{description} },
4036 sub { shift->{quantity} },
4037 sub { my($href, %opt) = @_;
4038 ($opt{dollar} || ''). $href->{amount};
4041 'align' => [ qw( l r r ) ],
4042 'span' => [ qw( 5 1 1 ) ], # unitprices?
4043 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4046 sub _condense_section {
4047 my ( $self, $format ) = ( shift, shift );
4049 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4050 qw( description_generator
4053 total_line_generator
4058 sub _condensed_generator_defaults {
4059 my ( $self, $format ) = ( shift, shift );
4060 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4069 sub _condensed_header_generator {
4070 my ( $self, $format ) = ( shift, shift );
4072 my ( $f, $prefix, $suffix, $separator, $column ) =
4073 _condensed_generator_defaults($format);
4075 if ($format eq 'latex') {
4076 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4077 $suffix = "\\\\\n\\hline";
4080 sub { my ($d,$a,$s,$w) = @_;
4081 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4083 } elsif ( $format eq 'html' ) {
4084 $prefix = '<th></th>';
4088 sub { my ($d,$a,$s,$w) = @_;
4089 return qq!<th align="$html_align{$a}">$d</th>!;
4097 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4099 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4102 $prefix. join($separator, @result). $suffix;
4107 sub _condensed_description_generator {
4108 my ( $self, $format ) = ( shift, shift );
4110 my ( $f, $prefix, $suffix, $separator, $column ) =
4111 _condensed_generator_defaults($format);
4113 my $money_char = '$';
4114 if ($format eq 'latex') {
4115 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4117 $separator = " & \n";
4119 sub { my ($d,$a,$s,$w) = @_;
4120 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4122 $money_char = '\\dollar';
4123 }elsif ( $format eq 'html' ) {
4124 $prefix = '"><td align="center"></td>';
4128 sub { my ($d,$a,$s,$w) = @_;
4129 return qq!<td align="$html_align{$a}">$d</td>!;
4131 #$money_char = $conf->config('money_char') || '$';
4132 $money_char = ''; # this is madness
4140 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4142 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4144 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4145 map { $f->{$_}->[$i] } qw(align span width)
4149 $prefix. join( $separator, @result ). $suffix;
4154 sub _condensed_total_generator {
4155 my ( $self, $format ) = ( shift, shift );
4157 my ( $f, $prefix, $suffix, $separator, $column ) =
4158 _condensed_generator_defaults($format);
4161 if ($format eq 'latex') {
4164 $separator = " & \n";
4166 sub { my ($d,$a,$s,$w) = @_;
4167 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4169 }elsif ( $format eq 'html' ) {
4173 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4175 sub { my ($d,$a,$s,$w) = @_;
4176 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4185 # my $r = &{$f->{fields}->[$i]}(@args);
4186 # $r .= ' Total' unless $i;
4188 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4190 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4191 map { $f->{$_}->[$i] } qw(align span width)
4195 $prefix. join( $separator, @result ). $suffix;
4200 =item total_line_generator FORMAT
4202 Returns a coderef used for generation of invoice total line items for this
4203 usage_class. FORMAT is either html or latex
4207 # should not be used: will have issues with hash element names (description vs
4208 # total_item and amount vs total_amount -- another array of functions?
4210 sub _condensed_total_line_generator {
4211 my ( $self, $format ) = ( shift, shift );
4213 my ( $f, $prefix, $suffix, $separator, $column ) =
4214 _condensed_generator_defaults($format);
4217 if ($format eq 'latex') {
4220 $separator = " & \n";
4222 sub { my ($d,$a,$s,$w) = @_;
4223 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4225 }elsif ( $format eq 'html' ) {
4229 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4231 sub { my ($d,$a,$s,$w) = @_;
4232 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4241 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4243 &{$column}( &{$f->{fields}->[$i]}(@args),
4244 map { $f->{$_}->[$i] } qw(align span width)
4248 $prefix. join( $separator, @result ). $suffix;
4253 #sub _items_extra_usage_sections {
4255 # my $escape = shift;
4257 # my %sections = ();
4259 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4260 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4262 # next unless $cust_bill_pkg->pkgnum > 0;
4264 # foreach my $section ( keys %usage_class ) {
4266 # my $usage = $cust_bill_pkg->usage($section);
4268 # next unless $usage && $usage > 0;
4270 # $sections{$section} ||= 0;
4271 # $sections{$section} += $usage;
4277 # map { { 'description' => &{$escape}($_),
4278 # 'subtotal' => $sections{$_},
4279 # 'summarized' => '',
4280 # 'tax_section' => '',
4283 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4287 sub _items_extra_usage_sections {
4289 my $conf = $self->conf;
4297 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4299 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4300 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4301 next unless $cust_bill_pkg->pkgnum > 0;
4303 foreach my $classnum ( keys %usage_class ) {
4304 my $section = $usage_class{$classnum}->classname;
4305 $classnums{$section} = $classnum;
4307 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4308 my $amount = $detail->amount;
4309 next unless $amount && $amount > 0;
4311 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4312 $sections{$section}{amount} += $amount; #subtotal
4313 $sections{$section}{calls}++;
4314 $sections{$section}{duration} += $detail->duration;
4316 my $desc = $detail->regionname;
4317 my $description = $desc;
4318 $description = substr($desc, 0, $maxlength). '...'
4319 if $format eq 'latex' && length($desc) > $maxlength;
4321 $lines{$section}{$desc} ||= {
4322 description => &{$escape}($description),
4323 #pkgpart => $part_pkg->pkgpart,
4324 pkgnum => $cust_bill_pkg->pkgnum,
4329 #unit_amount => $cust_bill_pkg->unitrecur,
4330 quantity => $cust_bill_pkg->quantity,
4331 product_code => 'N/A',
4332 ext_description => [],
4335 $lines{$section}{$desc}{amount} += $amount;
4336 $lines{$section}{$desc}{calls}++;
4337 $lines{$section}{$desc}{duration} += $detail->duration;
4343 my %sectionmap = ();
4344 foreach (keys %sections) {
4345 my $usage_class = $usage_class{$classnums{$_}};
4346 $sectionmap{$_} = { 'description' => &{$escape}($_),
4347 'amount' => $sections{$_}{amount}, #subtotal
4348 'calls' => $sections{$_}{calls},
4349 'duration' => $sections{$_}{duration},
4351 'tax_section' => '',
4352 'sort_weight' => $usage_class->weight,
4353 ( $usage_class->format
4354 ? ( map { $_ => $usage_class->$_($format) }
4355 qw( description_generator header_generator total_generator total_line_generator )
4362 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4366 foreach my $section ( keys %lines ) {
4367 foreach my $line ( keys %{$lines{$section}} ) {
4368 my $l = $lines{$section}{$line};
4369 $l->{section} = $sectionmap{$section};
4370 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4371 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4376 return(\@sections, \@lines);
4382 my $end = $self->_date;
4384 # start at date of previous invoice + 1 second or 0 if no previous invoice
4385 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4386 $start = 0 if !$start;
4389 my $cust_main = $self->cust_main;
4390 my @pkgs = $cust_main->all_pkgs;
4391 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4394 foreach my $pkg ( @pkgs ) {
4395 my @h_cust_svc = $pkg->h_cust_svc($end);
4396 foreach my $h_cust_svc ( @h_cust_svc ) {
4397 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4398 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4400 my $inserted = $h_cust_svc->date_inserted;
4401 my $deleted = $h_cust_svc->date_deleted;
4402 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4404 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4406 # DID either activated or ported in; cannot be both for same DID simultaneously
4407 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4408 && (!$phone_inserted->lnp_status
4409 || $phone_inserted->lnp_status eq ''
4410 || $phone_inserted->lnp_status eq 'native')) {
4413 else { # this one not so clean, should probably move to (h_)svc_phone
4414 my $phone_portedin = qsearchs( 'h_svc_phone',
4415 { 'svcnum' => $h_cust_svc->svcnum,
4416 'lnp_status' => 'portedin' },
4417 FS::h_svc_phone->sql_h_searchs($end),
4419 $num_portedin++ if $phone_portedin;
4422 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4423 if($deleted >= $start && $deleted <= $end && $phone_deleted
4424 && (!$phone_deleted->lnp_status
4425 || $phone_deleted->lnp_status ne 'portingout')) {
4428 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4429 && $phone_deleted->lnp_status
4430 && $phone_deleted->lnp_status eq 'portingout') {
4434 # increment usage minutes
4435 if ( $phone_inserted ) {
4436 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4437 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4440 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4443 # don't look at this service again
4444 push @seen, $h_cust_svc->svcnum;
4448 $minutes = sprintf("%d", $minutes);
4449 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4450 . "$num_deactivated Ported-Out: $num_portedout ",
4451 "Total Minutes: $minutes");
4454 sub _items_accountcode_cdr {
4459 my $section = { 'amount' => 0,
4462 'sort_weight' => '',
4464 'description' => 'Usage by Account Code',
4470 my %accountcodes = ();
4472 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4473 next unless $cust_bill_pkg->pkgnum > 0;
4475 my @header = $cust_bill_pkg->details_header;
4476 next unless scalar(@header);
4477 $section->{'header'} = join(',',@header);
4479 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4481 $section->{'header'} = $detail->formatted('format' => $format)
4482 if($detail->detail eq $section->{'header'});
4484 my $accountcode = $detail->accountcode;
4485 next unless $accountcode;
4487 my $amount = $detail->amount;
4488 next unless $amount && $amount > 0;
4490 $accountcodes{$accountcode} ||= {
4491 description => $accountcode,
4498 product_code => 'N/A',
4499 section => $section,
4500 ext_description => [ $section->{'header'} ],
4504 $section->{'amount'} += $amount;
4505 $accountcodes{$accountcode}{'amount'} += $amount;
4506 $accountcodes{$accountcode}{calls}++;
4507 $accountcodes{$accountcode}{duration} += $detail->duration;
4508 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4512 foreach my $l ( values %accountcodes ) {
4513 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4514 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4515 foreach my $sorted_detail ( @sorted_detail ) {
4516 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4518 delete $l->{detail_temp};
4522 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4524 return ($section,\@sorted_lines);
4527 sub _items_svc_phone_sections {
4529 my $conf = $self->conf;
4537 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4539 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4540 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4542 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4543 next unless $cust_bill_pkg->pkgnum > 0;
4545 my @header = $cust_bill_pkg->details_header;
4546 next unless scalar(@header);
4548 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4550 my $phonenum = $detail->phonenum;
4551 next unless $phonenum;
4553 my $amount = $detail->amount;
4554 next unless $amount && $amount > 0;
4556 $sections{$phonenum} ||= { 'amount' => 0,
4559 'sort_weight' => -1,
4560 'phonenum' => $phonenum,
4562 $sections{$phonenum}{amount} += $amount; #subtotal
4563 $sections{$phonenum}{calls}++;
4564 $sections{$phonenum}{duration} += $detail->duration;
4566 my $desc = $detail->regionname;
4567 my $description = $desc;
4568 $description = substr($desc, 0, $maxlength). '...'
4569 if $format eq 'latex' && length($desc) > $maxlength;
4571 $lines{$phonenum}{$desc} ||= {
4572 description => &{$escape}($description),
4573 #pkgpart => $part_pkg->pkgpart,
4581 product_code => 'N/A',
4582 ext_description => [],
4585 $lines{$phonenum}{$desc}{amount} += $amount;
4586 $lines{$phonenum}{$desc}{calls}++;
4587 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4589 my $line = $usage_class{$detail->classnum}->classname;
4590 $sections{"$phonenum $line"} ||=
4594 'sort_weight' => $usage_class{$detail->classnum}->weight,
4595 'phonenum' => $phonenum,
4596 'header' => [ @header ],
4598 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4599 $sections{"$phonenum $line"}{calls}++;
4600 $sections{"$phonenum $line"}{duration} += $detail->duration;
4602 $lines{"$phonenum $line"}{$desc} ||= {
4603 description => &{$escape}($description),
4604 #pkgpart => $part_pkg->pkgpart,
4612 product_code => 'N/A',
4613 ext_description => [],
4616 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4617 $lines{"$phonenum $line"}{$desc}{calls}++;
4618 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4619 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4620 $detail->formatted('format' => $format);
4625 my %sectionmap = ();
4626 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4627 foreach ( keys %sections ) {
4628 my @header = @{ $sections{$_}{header} || [] };
4630 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4631 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4632 my $usage_class = $summary ? $simple : $usage_simple;
4633 my $ending = $summary ? ' usage charges' : '';
4636 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4638 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4639 'amount' => $sections{$_}{amount}, #subtotal
4640 'calls' => $sections{$_}{calls},
4641 'duration' => $sections{$_}{duration},
4643 'tax_section' => '',
4644 'phonenum' => $sections{$_}{phonenum},
4645 'sort_weight' => $sections{$_}{sort_weight},
4646 'post_total' => $summary, #inspire pagebreak
4648 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4649 qw( description_generator
4652 total_line_generator
4659 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4660 $a->{sort_weight} <=> $b->{sort_weight}
4665 foreach my $section ( keys %lines ) {
4666 foreach my $line ( keys %{$lines{$section}} ) {
4667 my $l = $lines{$section}{$line};
4668 $l->{section} = $sectionmap{$section};
4669 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4670 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4675 if($conf->exists('phone_usage_class_summary')) {
4676 # this only works with Latex
4680 # after this, we'll have only two sections per DID:
4681 # Calls Summary and Calls Detail
4682 foreach my $section ( @sections ) {
4683 if($section->{'post_total'}) {
4684 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4685 $section->{'total_line_generator'} = sub { '' };
4686 $section->{'total_generator'} = sub { '' };
4687 $section->{'header_generator'} = sub { '' };
4688 $section->{'description_generator'} = '';
4689 push @newsections, $section;
4690 my %calls_detail = %$section;
4691 $calls_detail{'post_total'} = '';
4692 $calls_detail{'sort_weight'} = '';
4693 $calls_detail{'description_generator'} = sub { '' };
4694 $calls_detail{'header_generator'} = sub {
4695 return ' & Date/Time & Called Number & Duration & Price'
4696 if $format eq 'latex';
4699 $calls_detail{'description'} = 'Calls Detail: '
4700 . $section->{'phonenum'};
4701 push @newsections, \%calls_detail;
4705 # after this, each usage class is collapsed/summarized into a single
4706 # line under the Calls Summary section
4707 foreach my $newsection ( @newsections ) {
4708 if($newsection->{'post_total'}) { # this means Calls Summary
4709 foreach my $section ( @sections ) {
4710 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4711 && !$section->{'post_total'});
4712 my $newdesc = $section->{'description'};
4713 my $tn = $section->{'phonenum'};
4714 $newdesc =~ s/$tn//g;
4715 my $line = { ext_description => [],
4719 calls => $section->{'calls'},
4720 section => $newsection,
4721 duration => $section->{'duration'},
4722 description => $newdesc,
4723 amount => sprintf("%.2f",$section->{'amount'}),
4724 product_code => 'N/A',
4726 push @newlines, $line;
4731 # after this, Calls Details is populated with all CDRs
4732 foreach my $newsection ( @newsections ) {
4733 if(!$newsection->{'post_total'}) { # this means Calls Details
4734 foreach my $line ( @lines ) {
4735 next unless (scalar(@{$line->{'ext_description'}}) &&
4736 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4738 my @extdesc = @{$line->{'ext_description'}};
4740 foreach my $extdesc ( @extdesc ) {
4741 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4742 push @newextdesc, $extdesc;
4744 $line->{'ext_description'} = \@newextdesc;
4745 $line->{'section'} = $newsection;
4746 push @newlines, $line;
4751 return(\@newsections, \@newlines);
4754 return(\@sections, \@lines);
4758 sub _items { # seems to be unused
4761 #my @display = scalar(@_)
4763 # : qw( _items_previous _items_pkg );
4764 # #: qw( _items_pkg );
4765 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4766 my @display = qw( _items_previous _items_pkg );
4769 foreach my $display ( @display ) {
4770 push @b, $self->$display(@_);
4775 sub _items_previous {
4777 my $conf = $self->conf;
4778 my $cust_main = $self->cust_main;
4779 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4781 foreach ( @pr_cust_bill ) {
4782 my $date = $conf->exists('invoice_show_prior_due_date')
4783 ? 'due '. $_->due_date2str($date_format)
4784 : time2str($date_format, $_->_date);
4786 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4787 #'pkgpart' => 'N/A',
4789 'amount' => sprintf("%.2f", $_->owed),
4795 # 'description' => 'Previous Balance',
4796 # #'pkgpart' => 'N/A',
4797 # 'pkgnum' => 'N/A',
4798 # 'amount' => sprintf("%10.2f", $pr_total ),
4799 # 'ext_description' => [ map {
4800 # "Invoice ". $_->invnum.
4801 # " (". time2str("%x",$_->_date). ") ".
4802 # sprintf("%10.2f", $_->owed)
4803 # } @pr_cust_bill ],
4808 =item _items_pkg [ OPTIONS ]
4810 Return line item hashes for each package item on this invoice. Nearly
4813 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4815 The only OPTIONS accepted is 'section', which may point to a hashref
4816 with a key named 'condensed', which may have a true value. If it
4817 does, this method tries to merge identical items into items with
4818 'quantity' equal to the number of items (not the sum of their
4819 separate quantities, for some reason).
4827 warn "$me _items_pkg searching for all package line items\n"
4830 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4832 warn "$me _items_pkg filtering line items\n"
4834 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4836 if ($options{section} && $options{section}->{condensed}) {
4838 warn "$me _items_pkg condensing section\n"
4842 local $Storable::canonical = 1;
4843 foreach ( @items ) {
4845 delete $item->{ref};
4846 delete $item->{ext_description};
4847 my $key = freeze($item);
4848 $itemshash{$key} ||= 0;
4849 $itemshash{$key} ++; # += $item->{quantity};
4851 @items = sort { $a->{description} cmp $b->{description} }
4852 map { my $i = thaw($_);
4853 $i->{quantity} = $itemshash{$_};
4855 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4861 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4868 return 0 unless $a->itemdesc cmp $b->itemdesc;
4869 return -1 if $b->itemdesc eq 'Tax';
4870 return 1 if $a->itemdesc eq 'Tax';
4871 return -1 if $b->itemdesc eq 'Other surcharges';
4872 return 1 if $a->itemdesc eq 'Other surcharges';
4873 $a->itemdesc cmp $b->itemdesc;
4878 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4879 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4882 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4884 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4885 list of hashrefs describing the line items they generate on the invoice.
4887 OPTIONS may include:
4889 format: the invoice format.
4891 escape_function: the function used to escape strings.
4893 DEPRECATED? (expensive, mostly unused?)
4894 format_function: the function used to format CDRs.
4896 section: a hashref containing 'description'; if this is present,
4897 cust_bill_pkg_display records not belonging to this section are
4900 multisection: a flag indicating that this is a multisection invoice,
4901 which does something complicated.
4903 multilocation: a flag to display the location label for the package.
4905 Returns a list of hashrefs, each of which may contain:
4907 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4908 ext_description, which is an arrayref of detail lines to show below
4913 sub _items_cust_bill_pkg {
4915 my $conf = $self->conf;
4916 my $cust_bill_pkgs = shift;
4919 my $format = $opt{format} || '';
4920 my $escape_function = $opt{escape_function} || sub { shift };
4921 my $format_function = $opt{format_function} || '';
4922 my $no_usage = $opt{no_usage} || '';
4923 my $unsquelched = $opt{unsquelched} || ''; #unused
4924 my $section = $opt{section}->{description} if $opt{section};
4925 my $summary_page = $opt{summary_page} || ''; #unused
4926 my $multilocation = $opt{multilocation} || '';
4927 my $multisection = $opt{multisection} || '';
4928 my $discount_show_always = 0;
4930 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4932 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4935 my ($s, $r, $u) = ( undef, undef, undef );
4936 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4939 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4940 if ( $_ && !$cust_bill_pkg->hidden ) {
4941 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4942 $_->{amount} =~ s/^\-0\.00$/0.00/;
4943 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4945 if $_->{amount} != 0
4946 || $discount_show_always
4947 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4948 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4954 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4956 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4957 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4960 foreach my $display ( grep { defined($section)
4961 ? $_->section eq $section
4964 #grep { !$_->summary || !$summary_page } # bunk!
4965 grep { !$_->summary || $multisection }
4966 @cust_bill_pkg_display
4970 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4971 $display->billpkgdisplaynum. "\n"
4974 my $type = $display->type;
4976 my $desc = $cust_bill_pkg->desc;
4977 $desc = substr($desc, 0, $maxlength). '...'
4978 if $format eq 'latex' && length($desc) > $maxlength;
4980 my %details_opt = ( 'format' => $format,
4981 'escape_function' => $escape_function,
4982 'format_function' => $format_function,
4983 'no_usage' => $opt{'no_usage'},
4986 if ( $cust_bill_pkg->pkgnum > 0 ) {
4988 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4991 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4993 # start/end dates for invoice formats that do nonstandard
4995 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4997 if ( (!$type || $type eq 'S')
4998 && ( $cust_bill_pkg->setup != 0
4999 || $cust_bill_pkg->setup_show_zero
5004 warn "$me _items_cust_bill_pkg adding setup\n"
5007 my $description = $desc;
5008 $description .= ' Setup'
5009 if $cust_bill_pkg->recur != 0
5010 || $discount_show_always
5011 || $cust_bill_pkg->recur_show_zero;
5014 unless ( $cust_pkg->part_pkg->hide_svc_detail
5015 || $cust_bill_pkg->hidden )
5018 push @d, map &{$escape_function}($_),
5019 $cust_pkg->h_labels_short($self->_date, undef, 'I')
5020 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5022 if ( $multilocation ) {
5023 my $loc = $cust_pkg->location_label;
5024 $loc = substr($loc, 0, $maxlength). '...'
5025 if $format eq 'latex' && length($loc) > $maxlength;
5026 push @d, &{$escape_function}($loc);
5029 } #unless hiding service details
5031 push @d, $cust_bill_pkg->details(%details_opt)
5032 if $cust_bill_pkg->recur == 0;
5034 if ( $cust_bill_pkg->hidden ) {
5035 $s->{amount} += $cust_bill_pkg->setup;
5036 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5037 push @{ $s->{ext_description} }, @d;
5041 description => $description,
5042 #pkgpart => $part_pkg->pkgpart,
5043 pkgnum => $cust_bill_pkg->pkgnum,
5044 amount => $cust_bill_pkg->setup,
5045 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5046 unit_amount => $cust_bill_pkg->unitsetup,
5047 quantity => $cust_bill_pkg->quantity,
5048 ext_description => \@d,
5054 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5056 $cust_bill_pkg->recur != 0
5057 || $cust_bill_pkg->setup == 0
5058 || $discount_show_always
5059 || $cust_bill_pkg->recur_show_zero
5064 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5067 my $is_summary = $display->summary;
5068 my $description = ($is_summary && $type && $type eq 'U')
5069 ? "Usage charges" : $desc;
5071 #pry be a bit more efficient to look some of this conf stuff up
5074 $conf->exists('disable_line_item_date_ranges')
5075 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
5078 my $date_style = $conf->config( 'cust_bill-line_item-date_style',
5079 $cust_main->agentnum
5081 if ( defined($date_style) && $date_style eq 'month_of' ) {
5082 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5083 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5084 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5085 $cust_main->agentnum
5087 $desc .= ' ' unless $desc =~ /\s$/;
5088 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5090 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5091 " - ". time2str($date_format, $cust_bill_pkg->edate);
5093 $description .= " ($time_period)";
5097 my @seconds = (); # for display of usage info
5099 #at least until cust_bill_pkg has "past" ranges in addition to
5100 #the "future" sdate/edate ones... see #3032
5101 my @dates = ( $self->_date );
5102 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5103 push @dates, $prev->sdate if $prev;
5104 push @dates, undef if !$prev;
5106 unless ( $cust_pkg->part_pkg->hide_svc_detail
5107 || $cust_bill_pkg->itemdesc
5108 || $cust_bill_pkg->hidden
5109 || $is_summary && $type && $type eq 'U' )
5112 warn "$me _items_cust_bill_pkg adding service details\n"
5115 push @d, map &{$escape_function}($_),
5116 $cust_pkg->h_labels_short(@dates, 'I')
5117 #$cust_bill_pkg->edate,
5118 #$cust_bill_pkg->sdate)
5119 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5121 warn "$me _items_cust_bill_pkg done adding service details\n"
5124 if ( $multilocation ) {
5125 my $loc = $cust_pkg->location_label;
5126 $loc = substr($loc, 0, $maxlength). '...'
5127 if $format eq 'latex' && length($loc) > $maxlength;
5128 push @d, &{$escape_function}($loc);
5131 # Display of seconds_since_sqlradacct:
5132 # On the invoice, when processing @detail_items, look for a field
5133 # named 'seconds'. This will contain total seconds for each
5134 # service, in the same order as @ext_description. For services
5135 # that don't support this it will show undef.
5136 if ( $conf->exists('svc_acct-usage_seconds')
5137 and ! $cust_bill_pkg->pkgpart_override ) {
5138 foreach my $cust_svc (
5139 $cust_pkg->h_cust_svc(@dates, 'I')
5142 # eval because not having any part_export_usage exports
5143 # is a fatal error, last_bill/_date because that's how
5144 # sqlradius_hour billing does it
5146 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5148 push @seconds, $sec;
5150 } #if svc_acct-usage_seconds
5154 unless ( $is_summary ) {
5155 warn "$me _items_cust_bill_pkg adding details\n"
5158 #instead of omitting details entirely in this case (unwanted side
5159 # effects), just omit CDRs
5160 $details_opt{'no_usage'} = 1
5161 if $type && $type eq 'R';
5163 push @d, $cust_bill_pkg->details(%details_opt);
5166 warn "$me _items_cust_bill_pkg calculating amount\n"
5171 $amount = $cust_bill_pkg->recur;
5172 } elsif ($type eq 'R') {
5173 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5174 } elsif ($type eq 'U') {
5175 $amount = $cust_bill_pkg->usage;
5178 if ( !$type || $type eq 'R' ) {
5180 warn "$me _items_cust_bill_pkg adding recur\n"
5183 if ( $cust_bill_pkg->hidden ) {
5184 $r->{amount} += $amount;
5185 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5186 push @{ $r->{ext_description} }, @d;
5189 description => $description,
5190 #pkgpart => $part_pkg->pkgpart,
5191 pkgnum => $cust_bill_pkg->pkgnum,
5193 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5194 unit_amount => $cust_bill_pkg->unitrecur,
5195 quantity => $cust_bill_pkg->quantity,
5197 ext_description => \@d,
5199 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5202 } else { # $type eq 'U'
5204 warn "$me _items_cust_bill_pkg adding usage\n"
5207 if ( $cust_bill_pkg->hidden ) {
5208 $u->{amount} += $amount;
5209 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5210 push @{ $u->{ext_description} }, @d;
5213 description => $description,
5214 #pkgpart => $part_pkg->pkgpart,
5215 pkgnum => $cust_bill_pkg->pkgnum,
5217 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5218 unit_amount => $cust_bill_pkg->unitrecur,
5219 quantity => $cust_bill_pkg->quantity,
5221 ext_description => \@d,
5226 } # recurring or usage with recurring charge
5228 } else { #pkgnum tax or one-shot line item (??)
5230 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5233 if ( $cust_bill_pkg->setup != 0 ) {
5235 'description' => $desc,
5236 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5239 if ( $cust_bill_pkg->recur != 0 ) {
5241 'description' => "$desc (".
5242 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5243 time2str($date_format, $cust_bill_pkg->edate). ')',
5244 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5252 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5253 && $conf->exists('discount-show-always'));
5257 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5259 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5260 $_->{amount} =~ s/^\-0\.00$/0.00/;
5261 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5263 if $_->{amount} != 0
5264 || $discount_show_always
5265 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5266 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5270 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5277 sub _items_credits {
5278 my( $self, %opt ) = @_;
5279 my $trim_len = $opt{'trim_len'} || 60;
5283 foreach ( $self->cust_credited ) {
5285 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5287 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5288 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5289 $reason = " ($reason) " if $reason;
5292 #'description' => 'Credit ref\#'. $_->crednum.
5293 # " (". time2str("%x",$_->cust_credit->_date) .")".
5295 'description' => $self->mt('Credit applied').' '.
5296 time2str($date_format,$_->cust_credit->_date). $reason,
5297 'amount' => sprintf("%.2f",$_->amount),
5305 sub _items_payments {
5309 #get & print payments
5310 foreach ( $self->cust_bill_pay ) {
5312 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5315 'description' => $self->mt('Payment received').' '.
5316 time2str($date_format,$_->cust_pay->_date ),
5317 'amount' => sprintf("%.2f", $_->amount )
5325 =item _items_discounts_avail
5327 Returns an array of line item hashrefs representing available term discounts
5328 for this invoice. This makes the same assumptions that apply to term
5329 discounts in general: that the package is billed monthly, at a flat rate,
5330 with no usage charges. A prorated first month will be handled, as will
5331 a setup fee if the discount is allowed to apply to setup fees.
5335 sub _items_discounts_avail {
5337 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5339 my %plans = $self->discount_plans;
5341 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5345 my $plan = $plans{$months};
5347 my $term_total = sprintf('%.2f', $plan->discounted_total);
5348 my $percent = sprintf('%.0f',
5349 100 * (1 - $term_total / $plan->base_total) );
5350 my $permonth = sprintf('%.2f', $term_total / $months);
5351 my $detail = $self->mt('discount on item'). ' '.
5352 join(', ', map { "#$_" } $plan->pkgnums)
5355 # discounts for non-integer months don't work anyway
5356 $months = sprintf("%d", $months);
5359 description => $self->mt('Save [_1]% by paying for [_2] months',
5361 amount => $self->mt('[_1] ([_2] per month)',
5362 $term_total, $money_char.$permonth),
5363 ext_description => ($detail || ''),
5366 sort { $b <=> $a } keys %plans;
5370 =item call_details [ OPTION => VALUE ... ]
5372 Returns an array of CSV strings representing the call details for this invoice
5373 The only option available is the boolean prepend_billed_number
5378 my ($self, %opt) = @_;
5380 my $format_function = sub { shift };
5382 if ($opt{prepend_billed_number}) {
5383 $format_function = sub {
5387 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5392 my @details = map { $_->details( 'format_function' => $format_function,
5393 'escape_function' => sub{ return() },
5397 $self->cust_bill_pkg;
5398 my $header = $details[0];
5399 ( $header, grep { $_ ne $header } @details );
5409 =item process_reprint
5413 sub process_reprint {
5414 process_re_X('print', @_);
5417 =item process_reemail
5421 sub process_reemail {
5422 process_re_X('email', @_);
5430 process_re_X('fax', @_);
5438 process_re_X('ftp', @_);
5445 sub process_respool {
5446 process_re_X('spool', @_);
5449 use Storable qw(thaw);
5453 my( $method, $job ) = ( shift, shift );
5454 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5456 my $param = thaw(decode_base64(shift));
5457 warn Dumper($param) if $DEBUG;
5468 my($method, $job, %param ) = @_;
5470 warn "re_X $method for job $job with param:\n".
5471 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5474 #some false laziness w/search/cust_bill.html
5476 my $orderby = 'ORDER BY cust_bill._date';
5478 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5480 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5482 my @cust_bill = qsearch( {
5483 #'select' => "cust_bill.*",
5484 'table' => 'cust_bill',
5485 'addl_from' => $addl_from,
5487 'extra_sql' => $extra_sql,
5488 'order_by' => $orderby,
5492 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5494 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5497 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5498 foreach my $cust_bill ( @cust_bill ) {
5499 $cust_bill->$method();
5501 if ( $job ) { #progressbar foo
5503 if ( time - $min_sec > $last ) {
5504 my $error = $job->update_statustext(
5505 int( 100 * $num / scalar(@cust_bill) )
5507 die $error if $error;
5518 =head1 CLASS METHODS
5524 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5529 my ($class, $start, $end) = @_;
5531 $class->paid_sql($start, $end). ' - '.
5532 $class->credited_sql($start, $end);
5537 Returns an SQL fragment to retreive the net amount (charged minus credited).
5542 my ($class, $start, $end) = @_;
5543 'charged - '. $class->credited_sql($start, $end);
5548 Returns an SQL fragment to retreive the amount paid against this invoice.
5553 my ($class, $start, $end) = @_;
5554 $start &&= "AND cust_bill_pay._date <= $start";
5555 $end &&= "AND cust_bill_pay._date > $end";
5556 $start = '' unless defined($start);
5557 $end = '' unless defined($end);
5558 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5559 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5564 Returns an SQL fragment to retreive the amount credited against this invoice.
5569 my ($class, $start, $end) = @_;
5570 $start &&= "AND cust_credit_bill._date <= $start";
5571 $end &&= "AND cust_credit_bill._date > $end";
5572 $start = '' unless defined($start);
5573 $end = '' unless defined($end);
5574 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5575 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5580 Returns an SQL fragment to retrieve the due date of an invoice.
5581 Currently only supported on PostgreSQL.
5586 my $conf = new FS::Conf;
5590 cust_bill.invoice_terms,
5591 cust_main.invoice_terms,
5592 \''.($conf->config('invoice_default_terms') || '').'\'
5593 ), E\'Net (\\\\d+)\'
5595 ) * 86400 + cust_bill._date'
5598 =item search_sql_where HASHREF
5600 Class method which returns an SQL WHERE fragment to search for parameters
5601 specified in HASHREF. Valid parameters are
5607 List reference of start date, end date, as UNIX timestamps.
5617 List reference of charged limits (exclusive).
5621 List reference of charged limits (exclusive).
5625 flag, return open invoices only
5629 flag, return net invoices only
5633 =item newest_percust
5637 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5641 sub search_sql_where {
5642 my($class, $param) = @_;
5644 warn "$me search_sql_where called with params: \n".
5645 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5651 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5652 push @search, "cust_main.agentnum = $1";
5656 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5657 push @search, "cust_main.refnum = $1";
5661 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5662 push @search, "cust_bill.custnum = $1";
5666 if ( $param->{_date} ) {
5667 my($beginning, $ending) = @{$param->{_date}};
5669 push @search, "cust_bill._date >= $beginning",
5670 "cust_bill._date < $ending";
5674 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5675 push @search, "cust_bill.invnum >= $1";
5677 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5678 push @search, "cust_bill.invnum <= $1";
5682 if ( $param->{charged} ) {
5683 my @charged = ref($param->{charged})
5684 ? @{ $param->{charged} }
5685 : ($param->{charged});
5687 push @search, map { s/^charged/cust_bill.charged/; $_; }
5691 my $owed_sql = FS::cust_bill->owed_sql;
5694 if ( $param->{owed} ) {
5695 my @owed = ref($param->{owed})
5696 ? @{ $param->{owed} }
5698 push @search, map { s/^owed/$owed_sql/; $_; }
5703 push @search, "0 != $owed_sql"
5704 if $param->{'open'};
5705 push @search, '0 != '. FS::cust_bill->net_sql
5709 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5710 if $param->{'days'};
5713 if ( $param->{'newest_percust'} ) {
5715 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5716 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5718 my @newest_where = map { my $x = $_;
5719 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5722 grep ! /^cust_main./, @search;
5723 my $newest_where = scalar(@newest_where)
5724 ? ' AND '. join(' AND ', @newest_where)
5728 push @search, "cust_bill._date = (
5729 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5730 WHERE newest_cust_bill.custnum = cust_bill.custnum
5736 #promised_date - also has an option to accept nulls
5737 if ( $param->{promised_date} ) {
5738 my($beginning, $ending, $null) = @{$param->{promised_date}};
5740 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5741 "cust_bill.promised_date < $ending )" .
5742 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5745 #agent virtualization
5746 my $curuser = $FS::CurrentUser::CurrentUser;
5747 if ( $curuser->username eq 'fs_queue'
5748 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5750 my $newuser = qsearchs('access_user', {
5751 'username' => $username,
5755 $curuser = $newuser;
5757 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5760 push @search, $curuser->agentnums_sql;
5762 join(' AND ', @search );
5774 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5775 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base