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);
1545 $self->lpr_data(\%opt),
1546 'agentnum' => $self->cust_main->agentnum,
1551 =item fax_invoice HASHREF | [ TEMPLATE ]
1555 Options can be passed as a hashref (recommended) or as a single optional
1558 I<template>, if specified, is the name of a suffix for alternate invoices.
1560 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1566 return if $self->hide;
1567 my $conf = $self->conf;
1569 my( $template, $notice_name );
1572 $template = $opt->{'template'} || '';
1573 $notice_name = $opt->{'notice_name'} || 'Invoice';
1575 $template = scalar(@_) ? shift : '';
1576 $notice_name = 'Invoice';
1579 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1580 unless $conf->exists('invoice_latex');
1582 my $dialstring = $self->cust_main->getfield('fax');
1586 'template' => $template,
1587 'notice_name' => $notice_name,
1590 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1591 'dialstring' => $dialstring,
1593 die $error if $error;
1597 =item batch_invoice [ HASHREF ]
1599 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1600 isn't an open batch, one will be created.
1605 my ($self, $opt) = @_;
1606 my $bill_batch = $self->get_open_bill_batch;
1607 my $cust_bill_batch = FS::cust_bill_batch->new({
1608 batchnum => $bill_batch->batchnum,
1609 invnum => $self->invnum,
1611 return $cust_bill_batch->insert($opt);
1614 =item get_open_batch
1616 Returns the currently open batch as an FS::bill_batch object, creating a new
1617 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1622 sub get_open_bill_batch {
1624 my $conf = $self->conf;
1625 my $hashref = { status => 'O' };
1626 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1627 ? $self->cust_main->agentnum
1629 my $batch = qsearchs('bill_batch', $hashref);
1630 return $batch if $batch;
1631 $batch = FS::bill_batch->new($hashref);
1632 my $error = $batch->insert;
1633 die $error if $error;
1637 =item ftp_invoice [ TEMPLATENAME ]
1639 Sends this invoice data via FTP.
1641 TEMPLATENAME is unused?
1647 my $conf = $self->conf;
1648 my $template = scalar(@_) ? shift : '';
1651 'protocol' => 'ftp',
1652 'server' => $conf->config('cust_bill-ftpserver'),
1653 'username' => $conf->config('cust_bill-ftpusername'),
1654 'password' => $conf->config('cust_bill-ftppassword'),
1655 'dir' => $conf->config('cust_bill-ftpdir'),
1656 'format' => $conf->config('cust_bill-ftpformat'),
1660 =item spool_invoice [ TEMPLATENAME ]
1662 Spools this invoice data (see L<FS::spool_csv>)
1664 TEMPLATENAME is unused?
1670 my $conf = $self->conf;
1671 my $template = scalar(@_) ? shift : '';
1674 'format' => $conf->config('cust_bill-spoolformat'),
1675 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1679 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1681 Like B<send>, but only sends the invoice if it is the newest open invoice for
1686 sub send_if_newest {
1691 grep { $_->owed > 0 }
1692 qsearch('cust_bill', {
1693 'custnum' => $self->custnum,
1694 #'_date' => { op=>'>', value=>$self->_date },
1695 'invnum' => { op=>'>', value=>$self->invnum },
1702 =item send_csv OPTION => VALUE, ...
1704 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1708 protocol - currently only "ftp"
1714 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1715 and YYMMDDHHMMSS is a timestamp.
1717 See L</print_csv> for a description of the output format.
1722 my($self, %opt) = @_;
1726 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1727 mkdir $spooldir, 0700 unless -d $spooldir;
1729 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1730 my $file = "$spooldir/$tracctnum.csv";
1732 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1734 open(CSV, ">$file") or die "can't open $file: $!";
1742 if ( $opt{protocol} eq 'ftp' ) {
1743 eval "use Net::FTP;";
1745 $net = Net::FTP->new($opt{server}) or die @$;
1747 die "unknown protocol: $opt{protocol}";
1750 $net->login( $opt{username}, $opt{password} )
1751 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1753 $net->binary or die "can't set binary mode";
1755 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1757 $net->put($file) or die "can't put $file: $!";
1767 Spools CSV invoice data.
1773 =item format - 'default' or 'billco'
1775 =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>).
1777 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1779 =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.
1786 my($self, %opt) = @_;
1788 my $cust_main = $self->cust_main;
1790 if ( $opt{'dest'} ) {
1791 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1792 $cust_main->invoicing_list;
1793 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1794 || ! keys %invoicing_list;
1797 if ( $opt{'balanceover'} ) {
1799 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1802 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1803 mkdir $spooldir, 0700 unless -d $spooldir;
1805 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1809 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1810 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1813 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1815 open(CSV, ">>$file") or die "can't open $file: $!";
1816 flock(CSV, LOCK_EX);
1821 if ( lc($opt{'format'}) eq 'billco' ) {
1823 flock(CSV, LOCK_UN);
1828 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1831 open(CSV,">>$file") or die "can't open $file: $!";
1832 flock(CSV, LOCK_EX);
1838 flock(CSV, LOCK_UN);
1845 =item print_csv OPTION => VALUE, ...
1847 Returns CSV data for this invoice.
1851 format - 'default' or 'billco'
1853 Returns a list consisting of two scalars. The first is a single line of CSV
1854 header information for this invoice. The second is one or more lines of CSV
1855 detail information for this invoice.
1857 If I<format> is not specified or "default", the fields of the CSV file are as
1860 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1864 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1866 B<record_type> is C<cust_bill> for the initial header line only. The
1867 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1868 fields are filled in.
1870 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1871 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1874 =item invnum - invoice number
1876 =item custnum - customer number
1878 =item _date - invoice date
1880 =item charged - total invoice amount
1882 =item first - customer first name
1884 =item last - customer first name
1886 =item company - company name
1888 =item address1 - address line 1
1890 =item address2 - address line 1
1900 =item pkg - line item description
1902 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1904 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1906 =item sdate - start date for recurring fee
1908 =item edate - end date for recurring fee
1912 If I<format> is "billco", the fields of the header CSV file are as follows:
1914 +-------------------------------------------------------------------+
1915 | FORMAT HEADER FILE |
1916 |-------------------------------------------------------------------|
1917 | Field | Description | Name | Type | Width |
1918 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1919 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1920 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1921 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1922 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1923 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1924 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1925 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1926 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1927 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1928 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1929 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1930 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1931 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1932 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1933 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1934 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1935 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1936 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1937 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1938 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1939 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1940 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1941 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1942 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1943 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1944 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1945 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1946 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1947 +-------+-------------------------------+------------+------+-------+
1949 If I<format> is "billco", the fields of the detail CSV file are as follows:
1951 FORMAT FOR DETAIL FILE
1953 Field | Description | Name | Type | Width
1954 1 | N/A-Leave Empty | RC | CHAR | 2
1955 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1956 3 | Account Number | TRACCTNUM | CHAR | 15
1957 4 | Invoice Number | TRINVOICE | CHAR | 15
1958 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1959 6 | Transaction Detail | DETAILS | CHAR | 100
1960 7 | Amount | AMT | NUM* | 9
1961 8 | Line Format Control** | LNCTRL | CHAR | 2
1962 9 | Grouping Code | GROUP | CHAR | 2
1963 10 | User Defined | ACCT CODE | CHAR | 15
1968 my($self, %opt) = @_;
1970 eval "use Text::CSV_XS";
1973 my $cust_main = $self->cust_main;
1975 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1977 if ( lc($opt{'format'}) eq 'billco' ) {
1980 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1982 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1984 my( $previous_balance, @unused ) = $self->previous; #previous balance
1986 my $pmt_cr_applied = 0;
1987 $pmt_cr_applied += $_->{'amount'}
1988 foreach ( $self->_items_payments, $self->_items_credits ) ;
1990 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1993 '', # 1 | N/A-Leave Empty CHAR 2
1994 '', # 2 | N/A-Leave Empty CHAR 15
1995 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1996 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1997 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1998 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1999 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
2000 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
2001 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
2002 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
2003 '', # 10 | Ancillary Billing Information CHAR 30
2004 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
2005 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
2008 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
2011 $duedate, # 14 | Bill Due Date CHAR 10
2013 $previous_balance, # 15 | Previous Balance NUM* 9
2014 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
2015 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
2016 $totaldue, # 18 | Total Amt Due NUM* 9
2017 $totaldue, # 19 | Total Amt Due NUM* 9
2018 '', # 20 | 30 Day Aging NUM* 9
2019 '', # 21 | 60 Day Aging NUM* 9
2020 '', # 22 | 90 Day Aging NUM* 9
2021 'N', # 23 | Y/N CHAR 1
2022 '', # 24 | Remittance automation CHAR 100
2023 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
2024 $self->custnum, # 26 | Customer Reference Number CHAR 15
2025 '0', # 27 | Federal Tax*** NUM* 9
2026 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
2027 '0', # 29 | Other Taxes & Fees*** NUM* 9
2030 } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
2032 my ($previous_balance) = $self->previous;
2033 $previous_balance = sprintf('%.2f', $previous_balance);
2034 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2036 ($_->{pkgnum} || ''),
2039 } $self->_items_pkg;
2042 $cust_main->agentnum,
2043 $cust_main->agent->agent,
2047 $cust_main->address1,
2048 $cust_main->address2,
2054 time2str("%x", $self->_date),
2059 $self->due_date2str("%x"),
2070 time2str("%x", $self->_date),
2071 sprintf("%.2f", $self->charged),
2072 ( map { $cust_main->getfield($_) }
2073 qw( first last company address1 address2 city state zip country ) ),
2075 ) or die "can't create csv";
2078 my $header = $csv->string. "\n";
2081 if ( lc($opt{'format'}) eq 'billco' ) {
2084 foreach my $item ( $self->_items_pkg ) {
2087 '', # 1 | N/A-Leave Empty CHAR 2
2088 '', # 2 | N/A-Leave Empty CHAR 15
2089 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2090 $self->invnum, # 4 | Invoice Number CHAR 15
2091 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2092 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2093 $item->{'amount'}, # 7 | Amount NUM* 9
2094 '', # 8 | Line Format Control** CHAR 2
2095 '', # 9 | Grouping Code CHAR 2
2096 '', # 10 | User Defined CHAR 15
2099 $detail .= $csv->string. "\n";
2103 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2109 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2111 my($pkg, $setup, $recur, $sdate, $edate);
2112 if ( $cust_bill_pkg->pkgnum ) {
2114 ($pkg, $setup, $recur, $sdate, $edate) = (
2115 $cust_bill_pkg->part_pkg->pkg,
2116 ( $cust_bill_pkg->setup != 0
2117 ? sprintf("%.2f", $cust_bill_pkg->setup )
2119 ( $cust_bill_pkg->recur != 0
2120 ? sprintf("%.2f", $cust_bill_pkg->recur )
2122 ( $cust_bill_pkg->sdate
2123 ? time2str("%x", $cust_bill_pkg->sdate)
2125 ($cust_bill_pkg->edate
2126 ?time2str("%x", $cust_bill_pkg->edate)
2130 } else { #pkgnum tax
2131 next unless $cust_bill_pkg->setup != 0;
2132 $pkg = $cust_bill_pkg->desc;
2133 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2134 ( $sdate, $edate ) = ( '', '' );
2140 ( map { '' } (1..11) ),
2141 ($pkg, $setup, $recur, $sdate, $edate)
2142 ) or die "can't create csv";
2144 $detail .= $csv->string. "\n";
2150 ( $header, $detail );
2156 Pays this invoice with a compliemntary payment. If there is an error,
2157 returns the error, otherwise returns false.
2163 my $cust_pay = new FS::cust_pay ( {
2164 'invnum' => $self->invnum,
2165 'paid' => $self->owed,
2168 'payinfo' => $self->cust_main->payinfo,
2176 Attempts to pay this invoice with a credit card payment via a
2177 Business::OnlinePayment realtime gateway. See
2178 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2179 for supported processors.
2185 $self->realtime_bop( 'CC', @_ );
2190 Attempts to pay this invoice with an electronic check (ACH) payment via a
2191 Business::OnlinePayment realtime gateway. See
2192 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2193 for supported processors.
2199 $self->realtime_bop( 'ECHECK', @_ );
2204 Attempts to pay this invoice with phone bill (LEC) payment via a
2205 Business::OnlinePayment realtime gateway. See
2206 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2207 for supported processors.
2213 $self->realtime_bop( 'LEC', @_ );
2217 my( $self, $method ) = (shift,shift);
2218 my $conf = $self->conf;
2221 my $cust_main = $self->cust_main;
2222 my $balance = $cust_main->balance;
2223 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2224 $amount = sprintf("%.2f", $amount);
2225 return "not run (balance $balance)" unless $amount > 0;
2227 my $description = 'Internet Services';
2228 if ( $conf->exists('business-onlinepayment-description') ) {
2229 my $dtempl = $conf->config('business-onlinepayment-description');
2231 my $agent_obj = $cust_main->agent
2232 or die "can't retreive agent for $cust_main (agentnum ".
2233 $cust_main->agentnum. ")";
2234 my $agent = $agent_obj->agent;
2235 my $pkgs = join(', ',
2236 map { $_->part_pkg->pkg }
2237 grep { $_->pkgnum } $self->cust_bill_pkg
2239 $description = eval qq("$dtempl");
2242 $cust_main->realtime_bop($method, $amount,
2243 'description' => $description,
2244 'invnum' => $self->invnum,
2245 #this didn't do what we want, it just calls apply_payments_and_credits
2247 'apply_to_invoice' => 1,
2250 #this changes application behavior: auto payments
2251 #triggered against a specific invoice are now applied
2252 #to that invoice instead of oldest open.
2258 =item batch_card OPTION => VALUE...
2260 Adds a payment for this invoice to the pending credit card batch (see
2261 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2262 runs the payment using a realtime gateway.
2267 my ($self, %options) = @_;
2268 my $cust_main = $self->cust_main;
2270 $options{invnum} = $self->invnum;
2272 $cust_main->batch_card(%options);
2275 sub _agent_template {
2277 $self->cust_main->agent_template;
2280 sub _agent_invoice_from {
2282 $self->cust_main->agent_invoice_from;
2285 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2287 Returns an text invoice, as a list of lines.
2289 Options can be passed as a hashref (recommended) or as a list of time, template
2290 and then any key/value pairs for any other options.
2292 I<time>, if specified, is used to control the printing of overdue messages. The
2293 default is now. It isn't the date of the invoice; that's the `_date' field.
2294 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2295 L<Time::Local> and L<Date::Parse> for conversion functions.
2297 I<template>, if specified, is the name of a suffix for alternate invoices.
2299 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2305 my( $today, $template, %opt );
2307 %opt = %{ shift() };
2308 $today = delete($opt{'time'}) || '';
2309 $template = delete($opt{template}) || '';
2311 ( $today, $template, %opt ) = @_;
2314 my %params = ( 'format' => 'template' );
2315 $params{'time'} = $today if $today;
2316 $params{'template'} = $template if $template;
2317 $params{$_} = $opt{$_}
2318 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2320 $self->print_generic( %params );
2323 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2325 Internal method - returns a filename of a filled-in LaTeX template for this
2326 invoice (Note: add ".tex" to get the actual filename), and a filename of
2327 an associated logo (with the .eps extension included).
2329 See print_ps and print_pdf for methods that return PostScript and PDF output.
2331 Options can be passed as a hashref (recommended) or as a list of time, template
2332 and then any key/value pairs for any other options.
2334 I<time>, if specified, is used to control the printing of overdue messages. The
2335 default is now. It isn't the date of the invoice; that's the `_date' field.
2336 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2337 L<Time::Local> and L<Date::Parse> for conversion functions.
2339 I<template>, if specified, is the name of a suffix for alternate invoices.
2341 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2347 my $conf = $self->conf;
2348 my( $today, $template, %opt );
2350 %opt = %{ shift() };
2351 $today = delete($opt{'time'}) || '';
2352 $template = delete($opt{template}) || '';
2354 ( $today, $template, %opt ) = @_;
2357 my %params = ( 'format' => 'latex' );
2358 $params{'time'} = $today if $today;
2359 $params{'template'} = $template if $template;
2360 $params{$_} = $opt{$_}
2361 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2363 $template ||= $self->_agent_template;
2365 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2366 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2370 ) or die "can't open temp file: $!\n";
2372 my $agentnum = $self->cust_main->agentnum;
2374 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2375 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2376 or die "can't write temp file: $!\n";
2378 print $lh $conf->config_binary('logo.eps', $agentnum)
2379 or die "can't write temp file: $!\n";
2382 $params{'logo_file'} = $lh->filename;
2384 if($conf->exists('invoice-barcode')){
2385 my $png_file = $self->invoice_barcode($dir);
2386 my $eps_file = $png_file;
2387 $eps_file =~ s/\.png$/.eps/g;
2388 $png_file =~ /(barcode.*png)/;
2390 $eps_file =~ /(barcode.*eps)/;
2393 my $curr_dir = cwd();
2395 # after painfuly long experimentation, it was determined that sam2p won't
2396 # accept : and other chars in the path, no matter how hard I tried to
2397 # escape them, hence the chdir (and chdir back, just to be safe)
2398 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2399 or die "sam2p failed: $!\n";
2403 $params{'barcode_file'} = $eps_file;
2406 my @filled_in = $self->print_generic( %params );
2408 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2412 ) or die "can't open temp file: $!\n";
2413 binmode($fh, ':utf8'); # language support
2414 print $fh join('', @filled_in );
2417 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2418 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2422 =item invoice_barcode DIR_OR_FALSE
2424 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2425 it is taken as the temp directory where the PNG file will be generated and the
2426 PNG file name is returned. Otherwise, the PNG image itself is returned.
2430 sub invoice_barcode {
2431 my ($self, $dir) = (shift,shift);
2433 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2434 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2435 my $gd = $gdbar->plot(Height => 30);
2438 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2442 ) or die "can't open temp file: $!\n";
2443 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2444 my $png_file = $bh->filename;
2451 =item print_generic OPTION => VALUE ...
2453 Internal method - returns a filled-in template for this invoice as a scalar.
2455 See print_ps and print_pdf for methods that return PostScript and PDF output.
2457 Non optional options include
2458 format - latex, html, template
2460 Optional options include
2462 template - a value used as a suffix for a configuration template
2464 time - a value used to control the printing of overdue messages. The
2465 default is now. It isn't the date of the invoice; that's the `_date' field.
2466 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2467 L<Time::Local> and L<Date::Parse> for conversion functions.
2471 unsquelch_cdr - overrides any per customer cdr squelching when true
2473 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2475 locale - override customer's locale
2479 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2480 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2481 # yes: fixed width/plain text printing will be borked
2483 my( $self, %params ) = @_;
2484 my $conf = $self->conf;
2485 my $today = $params{today} ? $params{today} : time;
2486 warn "$me print_generic called on $self with suffix $params{template}\n"
2489 my $format = $params{format};
2490 die "Unknown format: $format"
2491 unless $format =~ /^(latex|html|template)$/;
2493 my $cust_main = $self->cust_main;
2494 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2495 unless $cust_main->payname
2496 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2498 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2499 'html' => [ '<%=', '%>' ],
2500 'template' => [ '{', '}' ],
2503 warn "$me print_generic creating template\n"
2506 #create the template
2507 my $template = $params{template} ? $params{template} : $self->_agent_template;
2508 my $templatefile = "invoice_$format";
2509 $templatefile .= "_$template"
2510 if length($template) && $conf->exists($templatefile."_$template");
2511 my @invoice_template = map "$_\n", $conf->config($templatefile)
2512 or die "cannot load config data $templatefile";
2515 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2516 #change this to a die when the old code is removed
2517 warn "old-style invoice template $templatefile; ".
2518 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2519 $old_latex = 'true';
2520 @invoice_template = _translate_old_latex_format(@invoice_template);
2523 warn "$me print_generic creating T:T object\n"
2526 my $text_template = new Text::Template(
2528 SOURCE => \@invoice_template,
2529 DELIMITERS => $delimiters{$format},
2532 warn "$me print_generic compiling T:T object\n"
2535 $text_template->compile()
2536 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2539 # additional substitution could possibly cause breakage in existing templates
2540 my %convert_maps = (
2542 'notes' => sub { map "$_", @_ },
2543 'footer' => sub { map "$_", @_ },
2544 'smallfooter' => sub { map "$_", @_ },
2545 'returnaddress' => sub { map "$_", @_ },
2546 'coupon' => sub { map "$_", @_ },
2547 'summary' => sub { map "$_", @_ },
2553 s/%%(.*)$/<!-- $1 -->/g;
2554 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2555 s/\\begin\{enumerate\}/<ol>/g;
2557 s/\\end\{enumerate\}/<\/ol>/g;
2558 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2567 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2569 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2574 s/\\\\\*?\s*$/<BR>/;
2575 s/\\hyphenation\{[\w\s\-]+}//;
2580 'coupon' => sub { "" },
2581 'summary' => sub { "" },
2588 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2589 s/\\begin\{enumerate\}//g;
2591 s/\\end\{enumerate\}//g;
2592 s/\\textbf\{(.*)\}/$1/g;
2599 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2601 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2606 s/\\\\\*?\s*$/\n/; # dubious
2607 s/\\hyphenation\{[\w\s\-]+}//;
2611 'coupon' => sub { "" },
2612 'summary' => sub { "" },
2617 # hashes for differing output formats
2618 my %nbsps = ( 'latex' => '~',
2619 'html' => '', # '&nbps;' would be nice
2620 'template' => '', # not used
2622 my $nbsp = $nbsps{$format};
2624 my %escape_functions = ( 'latex' => \&_latex_escape,
2625 'html' => \&_html_escape_nbsp,#\&encode_entities,
2626 'template' => sub { shift },
2628 my $escape_function = $escape_functions{$format};
2629 my $escape_function_nonbsp = ($format eq 'html')
2630 ? \&_html_escape : $escape_function;
2632 my %date_formats = ( 'latex' => $date_format_long,
2633 'html' => $date_format_long,
2636 $date_formats{'html'} =~ s/ / /g;
2638 my $date_format = $date_formats{$format};
2640 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2642 'html' => sub { return '<b>'. shift(). '</b>'
2644 'template' => sub { shift },
2646 my $embolden_function = $embolden_functions{$format};
2648 my %newline_tokens = ( 'latex' => '\\\\',
2652 my $newline_token = $newline_tokens{$format};
2654 warn "$me generating template variables\n"
2657 # generate template variables
2660 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2664 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2670 $returnaddress = join("\n",
2671 $conf->config_orbase("invoice_${format}returnaddress", $template)
2674 } elsif ( grep /\S/,
2675 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2677 my $convert_map = $convert_maps{$format}{'returnaddress'};
2680 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2685 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2687 my $convert_map = $convert_maps{$format}{'returnaddress'};
2688 $returnaddress = join( "\n", &$convert_map(
2689 map { s/( {2,})/'~' x length($1)/eg;
2693 ( $conf->config('company_name', $self->cust_main->agentnum),
2694 $conf->config('company_address', $self->cust_main->agentnum),
2701 my $warning = "Couldn't find a return address; ".
2702 "do you need to set the company_address configuration value?";
2704 $returnaddress = $nbsp;
2705 #$returnaddress = $warning;
2709 warn "$me generating invoice data\n"
2712 my $agentnum = $self->cust_main->agentnum;
2714 my %invoice_data = (
2717 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2718 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2719 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2720 'returnaddress' => $returnaddress,
2721 'agent' => &$escape_function($cust_main->agent->agent),
2724 'invnum' => $self->invnum,
2725 'date' => time2str($date_format, $self->_date),
2726 'today' => time2str($date_format_long, $today),
2727 'terms' => $self->terms,
2728 'template' => $template, #params{'template'},
2729 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2730 'current_charges' => sprintf("%.2f", $self->charged),
2731 'duedate' => $self->due_date2str($rdate_format), #date_format?
2734 'custnum' => $cust_main->display_custnum,
2735 'agent_custid' => &$escape_function($cust_main->agent_custid),
2736 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2737 payname company address1 address2 city state zip fax
2741 'ship_enable' => $conf->exists('invoice-ship_address'),
2742 'unitprices' => $conf->exists('invoice-unitprice'),
2743 'smallernotes' => $conf->exists('invoice-smallernotes'),
2744 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2745 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2747 #layout info -- would be fancy to calc some of this and bury the template
2749 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2750 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2751 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2752 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2753 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2754 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2755 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2756 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2757 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2758 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2760 # better hang on to conf_dir for a while (for old templates)
2761 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2763 #these are only used when doing paged plaintext
2770 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2771 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2772 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2773 # eval to avoid death for unimplemented languages
2774 my $dh = eval { Date::Language->new($info{'name'}) } ||
2775 Date::Language->new(); # fall back to English
2776 # prototype here to silence warnings
2777 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2778 # eventually use this date handle everywhere in here, too
2780 my $min_sdate = 999999999999;
2782 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2783 next unless $cust_bill_pkg->pkgnum > 0;
2784 $min_sdate = $cust_bill_pkg->sdate
2785 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2786 $max_edate = $cust_bill_pkg->edate
2787 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2790 $invoice_data{'bill_period'} = '';
2791 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2792 . " to " . time2str('%e %h', $max_edate)
2793 if ($max_edate != 0 && $min_sdate != 999999999999);
2795 $invoice_data{finance_section} = '';
2796 if ( $conf->config('finance_pkgclass') ) {
2798 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2799 $invoice_data{finance_section} = $pkg_class->categoryname;
2801 $invoice_data{finance_amount} = '0.00';
2802 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2804 my $countrydefault = $conf->config('countrydefault') || 'US';
2805 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2806 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2807 my $method = $prefix.$_;
2808 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2810 $invoice_data{'ship_country'} = ''
2811 if ( $invoice_data{'ship_country'} eq $countrydefault );
2813 $invoice_data{'cid'} = $params{'cid'}
2816 if ( $cust_main->country eq $countrydefault ) {
2817 $invoice_data{'country'} = '';
2819 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2823 $invoice_data{'address'} = \@address;
2825 $cust_main->payname.
2826 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2827 ? " (P.O. #". $cust_main->payinfo. ")"
2831 push @address, $cust_main->company
2832 if $cust_main->company;
2833 push @address, $cust_main->address1;
2834 push @address, $cust_main->address2
2835 if $cust_main->address2;
2837 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2838 push @address, $invoice_data{'country'}
2839 if $invoice_data{'country'};
2841 while (scalar(@address) < 5);
2843 $invoice_data{'logo_file'} = $params{'logo_file'}
2844 if $params{'logo_file'};
2845 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2846 if $params{'barcode_file'};
2847 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2848 if $params{'barcode_img'};
2849 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2850 if $params{'barcode_cid'};
2852 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2853 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2854 #my $balance_due = $self->owed + $pr_total - $cr_total;
2855 my $balance_due = $self->owed + $pr_total;
2857 # the customer's current balance as shown on the invoice before this one
2858 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2860 # the change in balance from that invoice to this one
2861 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2863 # the sum of amount owed on all previous invoices
2864 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2866 # the sum of amount owed on all invoices
2867 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2869 # info from customer's last invoice before this one, for some
2871 $invoice_data{'last_bill'} = {};
2872 my $last_bill = $pr_cust_bill[-1];
2874 $invoice_data{'last_bill'} = {
2875 '_date' => $last_bill->_date, #unformatted
2876 # all we need for now
2880 my $summarypage = '';
2881 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2884 $invoice_data{'summarypage'} = $summarypage;
2886 warn "$me substituting variables in notes, footer, smallfooter\n"
2889 my @include = (qw( notes footer smallfooter ));
2890 push @include, 'coupon' unless $params{'no_coupon'};
2891 foreach my $include (@include) {
2893 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2896 if ( $conf->exists($inc_file, $agentnum)
2897 && length( $conf->config($inc_file, $agentnum) ) ) {
2899 @inc_src = $conf->config($inc_file, $agentnum);
2903 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2905 my $convert_map = $convert_maps{$format}{$include};
2907 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2908 s/--\@\]/$delimiters{$format}[1]/g;
2911 &$convert_map( $conf->config($inc_file, $agentnum) );
2915 my $inc_tt = new Text::Template (
2917 SOURCE => [ map "$_\n", @inc_src ],
2918 DELIMITERS => $delimiters{$format},
2919 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2921 unless ( $inc_tt->compile() ) {
2922 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2923 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2927 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2929 $invoice_data{$include} =~ s/\n+$//
2930 if ($format eq 'latex');
2933 # let invoices use either of these as needed
2934 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2935 ? $cust_main->payinfo : '';
2936 $invoice_data{'po_line'} =
2937 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2938 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2941 my %money_chars = ( 'latex' => '',
2942 'html' => $conf->config('money_char') || '$',
2945 my $money_char = $money_chars{$format};
2947 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2948 'html' => $conf->config('money_char') || '$',
2951 my $other_money_char = $other_money_chars{$format};
2952 $invoice_data{'dollar'} = $other_money_char;
2954 my @detail_items = ();
2955 my @total_items = ();
2959 $invoice_data{'detail_items'} = \@detail_items;
2960 $invoice_data{'total_items'} = \@total_items;
2961 $invoice_data{'buf'} = \@buf;
2962 $invoice_data{'sections'} = \@sections;
2964 warn "$me generating sections\n"
2967 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2968 'subtotal' => $other_money_char.
2969 sprintf('%.2f', $pr_total),
2970 'summarized' => '', #why? $summarypage ? 'Y' : '',
2972 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2973 join(' / ', map { $cust_main->balance_date_range(@$_) }
2974 $self->_prior_month30s
2976 if $conf->exists('invoice_include_aging');
2979 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2980 'subtotal' => $taxtotal, # adjusted below
2982 my $tax_weight = _pkg_category($tax_section->{description})
2983 ? _pkg_category($tax_section->{description})->weight
2985 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2986 $tax_section->{'sort_weight'} = $tax_weight;
2989 my $adjusttotal = 0;
2990 my $adjust_section = {
2991 'description' => $self->mt('Credits, Payments, and Adjustments'),
2992 'adjust_section' => 1,
2993 'subtotal' => 0, # adjusted below
2995 my $adjust_weight = _pkg_category($adjust_section->{description})
2996 ? _pkg_category($adjust_section->{description})->weight
2998 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2999 $adjust_section->{'sort_weight'} = $adjust_weight;
3001 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
3002 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
3003 $invoice_data{'multisection'} = $multisection;
3004 my $late_sections = [];
3005 my $extra_sections = [];
3006 my $extra_lines = ();
3008 my $default_section = { 'description' => '',
3013 if ( $multisection ) {
3014 ($extra_sections, $extra_lines) =
3015 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
3016 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
3018 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
3020 push @detail_items, @$extra_lines if $extra_lines;
3022 $self->_items_sections( $late_sections, # this could stand a refactor
3024 $escape_function_nonbsp,
3028 if ($conf->exists('svc_phone_sections')) {
3029 my ($phone_sections, $phone_lines) =
3030 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
3031 push @{$late_sections}, @$phone_sections;
3032 push @detail_items, @$phone_lines;
3034 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
3035 my ($accountcode_section, $accountcode_lines) =
3036 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
3037 if ( scalar(@$accountcode_lines) ) {
3038 push @{$late_sections}, $accountcode_section;
3039 push @detail_items, @$accountcode_lines;
3042 } else {# not multisection
3043 # make a default section
3044 push @sections, $default_section;
3045 # and calculate the finance charge total, since it won't get done otherwise.
3046 # XXX possibly other totals?
3047 # XXX possibly finance_pkgclass should not be used in this manner?
3048 if ( $conf->exists('finance_pkgclass') ) {
3049 my @finance_charges;
3050 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3051 if ( grep { $_->section eq $invoice_data{finance_section} }
3052 $cust_bill_pkg->cust_bill_pkg_display ) {
3053 # I think these are always setup fees, but just to be sure...
3054 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
3057 $invoice_data{finance_amount} =
3058 sprintf('%.2f', sum( @finance_charges ) || 0);
3062 # previous invoice balances in the Previous Charges section if there
3063 # is one, otherwise in the main detail section
3064 if ( $self->can('_items_previous') &&
3065 $self->enable_previous &&
3066 ! $conf->exists('previous_balance-summary_only') ) {
3068 warn "$me adding previous balances\n"
3071 foreach my $line_item ( $self->_items_previous ) {
3074 ext_description => [],
3076 $detail->{'ref'} = $line_item->{'pkgnum'};
3077 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3078 $detail->{'quantity'} = 1;
3079 $detail->{'section'} = $multisection ? $previous_section
3081 $detail->{'description'} = &$escape_function($line_item->{'description'});
3082 if ( exists $line_item->{'ext_description'} ) {
3083 @{$detail->{'ext_description'}} = map {
3084 &$escape_function($_);
3085 } @{$line_item->{'ext_description'}};
3087 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3088 $line_item->{'amount'};
3089 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3091 push @detail_items, $detail;
3092 push @buf, [ $detail->{'description'},
3093 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3099 if ( @pr_cust_bill && $self->enable_previous ) {
3100 push @buf, ['','-----------'];
3101 push @buf, [ $self->mt('Total Previous Balance'),
3102 $money_char. sprintf("%10.2f", $pr_total) ];
3106 if ( $conf->exists('svc_phone-did-summary') ) {
3107 warn "$me adding DID summary\n"
3110 my ($didsummary,$minutes) = $self->_did_summary;
3111 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3113 { 'description' => $didsummary_desc,
3114 'ext_description' => [ $didsummary, $minutes ],
3118 foreach my $section (@sections, @$late_sections) {
3120 warn "$me adding section \n". Dumper($section)
3123 # begin some normalization
3124 $section->{'subtotal'} = $section->{'amount'}
3126 && !exists($section->{subtotal})
3127 && exists($section->{amount});
3129 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3130 if ( $invoice_data{finance_section} &&
3131 $section->{'description'} eq $invoice_data{finance_section} );
3133 $section->{'subtotal'} = $other_money_char.
3134 sprintf('%.2f', $section->{'subtotal'})
3137 # continue some normalization
3138 $section->{'amount'} = $section->{'subtotal'}
3142 if ( $section->{'description'} ) {
3143 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3148 warn "$me setting options\n"
3151 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3153 $options{'section'} = $section if $multisection;
3154 $options{'format'} = $format;
3155 $options{'escape_function'} = $escape_function;
3156 $options{'no_usage'} = 1 unless $unsquelched;
3157 $options{'unsquelched'} = $unsquelched;
3158 $options{'summary_page'} = $summarypage;
3159 $options{'skip_usage'} =
3160 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3161 $options{'multilocation'} = $multilocation;
3162 $options{'multisection'} = $multisection;
3164 warn "$me searching for line items\n"
3167 foreach my $line_item ( $self->_items_pkg(%options) ) {
3169 warn "$me adding line item $line_item\n"
3173 ext_description => [],
3175 $detail->{'ref'} = $line_item->{'pkgnum'};
3176 $detail->{'pkgpart'} = $line_item->{'pkgpart'};
3177 $detail->{'quantity'} = $line_item->{'quantity'};
3178 $detail->{'section'} = $section;
3179 $detail->{'description'} = &$escape_function($line_item->{'description'});
3180 if ( exists $line_item->{'ext_description'} ) {
3181 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3183 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3184 $line_item->{'amount'};
3185 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3186 $line_item->{'unit_amount'};
3187 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3189 $detail->{'sdate'} = $line_item->{'sdate'};
3190 $detail->{'edate'} = $line_item->{'edate'};
3191 $detail->{'seconds'} = $line_item->{'seconds'};
3192 $detail->{'svc_label'} = $line_item->{'svc_label'};
3194 push @detail_items, $detail;
3195 push @buf, ( [ $detail->{'description'},
3196 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3198 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3202 if ( $section->{'description'} ) {
3203 push @buf, ( ['','-----------'],
3204 [ $section->{'description'}. ' sub-total',
3205 $section->{'subtotal'} # already formatted this
3214 $invoice_data{current_less_finance} =
3215 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3217 # create a major section for previous balance if we have major sections,
3218 # or if previous_section is in summary form
3219 if ( ( $multisection && $self->enable_previous )
3220 || $conf->exists('previous_balance-summary_only') )
3222 unshift @sections, $previous_section if $pr_total;
3225 warn "$me adding taxes\n"
3228 foreach my $tax ( $self->_items_tax ) {
3230 $taxtotal += $tax->{'amount'};
3232 my $description = &$escape_function( $tax->{'description'} );
3233 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3235 if ( $multisection ) {
3237 my $money = $old_latex ? '' : $money_char;
3238 push @detail_items, {
3239 ext_description => [],
3242 description => $description,
3243 amount => $money. $amount,
3245 section => $tax_section,
3250 push @total_items, {
3251 'total_item' => $description,
3252 'total_amount' => $other_money_char. $amount,
3257 push @buf,[ $description,
3258 $money_char. $amount,
3265 $total->{'total_item'} = $self->mt('Sub-total');
3266 $total->{'total_amount'} =
3267 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3269 if ( $multisection ) {
3270 $tax_section->{'subtotal'} = $other_money_char.
3271 sprintf('%.2f', $taxtotal);
3272 $tax_section->{'pretotal'} = 'New charges sub-total '.
3273 $total->{'total_amount'};
3274 push @sections, $tax_section if $taxtotal;
3276 unshift @total_items, $total;
3279 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3281 push @buf,['','-----------'];
3282 push @buf,[$self->mt(
3283 (!$self->enable_previous)
3285 : 'Total New Charges'
3287 $money_char. sprintf("%10.2f",$self->charged) ];
3290 # calculate total, possibly including total owed on previous
3295 $item = $conf->config('previous_balance-exclude_from_total')
3296 || 'Total New Charges'
3297 if $conf->exists('previous_balance-exclude_from_total');
3298 my $amount = $self->charged;
3299 if ( $self->enable_previous and !$conf->exists('previous_balance-exclude_from_total') ) {
3300 $amount += $pr_total;
3303 $total->{'total_item'} = &$embolden_function($self->mt($item));
3304 $total->{'total_amount'} =
3305 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3306 if ( $multisection ) {
3307 if ( $adjust_section->{'sort_weight'} ) {
3308 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3309 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3311 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3312 $other_money_char. sprintf('%.2f', $self->charged );
3315 push @total_items, $total;
3317 push @buf,['','-----------'];
3320 sprintf( '%10.2f', $amount )
3325 # if we're showing previous invoices, also show previous
3326 # credits and payments
3327 if ( $self->enable_previous
3328 and $self->can('_items_credits')
3329 and $self->can('_items_payments') )
3331 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3334 my $credittotal = 0;
3335 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3338 $total->{'total_item'} = &$escape_function($credit->{'description'});
3339 $credittotal += $credit->{'amount'};
3340 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3341 $adjusttotal += $credit->{'amount'};
3342 if ( $multisection ) {
3343 my $money = $old_latex ? '' : $money_char;
3344 push @detail_items, {
3345 ext_description => [],
3348 description => &$escape_function($credit->{'description'}),
3349 amount => $money. $credit->{'amount'},
3351 section => $adjust_section,
3354 push @total_items, $total;
3358 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3361 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3362 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3366 my $paymenttotal = 0;
3367 foreach my $payment ( $self->_items_payments ) {
3369 $total->{'total_item'} = &$escape_function($payment->{'description'});
3370 $paymenttotal += $payment->{'amount'};
3371 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3372 $adjusttotal += $payment->{'amount'};
3373 if ( $multisection ) {
3374 my $money = $old_latex ? '' : $money_char;
3375 push @detail_items, {
3376 ext_description => [],
3379 description => &$escape_function($payment->{'description'}),
3380 amount => $money. $payment->{'amount'},
3382 section => $adjust_section,
3385 push @total_items, $total;
3387 push @buf, [ $payment->{'description'},
3388 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3391 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3393 if ( $multisection ) {
3394 $adjust_section->{'subtotal'} = $other_money_char.
3395 sprintf('%.2f', $adjusttotal);
3396 push @sections, $adjust_section
3397 unless $adjust_section->{sort_weight};
3400 # create Balance Due message
3403 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3404 $total->{'total_amount'} =
3405 &$embolden_function(
3406 $other_money_char. sprintf('%.2f', #why? $summarypage
3407 # ? $self->charged +
3408 # $self->billing_balance
3410 $self->owed + $pr_total
3413 if ( $multisection && !$adjust_section->{sort_weight} ) {
3414 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3415 $total->{'total_amount'};
3417 push @total_items, $total;
3419 push @buf,['','-----------'];
3420 push @buf,[$self->balance_due_msg, $money_char.
3421 sprintf("%10.2f", $balance_due ) ];
3424 if ( $conf->exists('previous_balance-show_credit')
3425 and $cust_main->balance < 0 ) {
3426 my $credit_total = {
3427 'total_item' => &$embolden_function($self->credit_balance_msg),
3428 'total_amount' => &$embolden_function(
3429 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3432 if ( $multisection ) {
3433 $adjust_section->{'posttotal'} .= $newline_token .
3434 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3437 push @total_items, $credit_total;
3439 push @buf,['','-----------'];
3440 push @buf,[$self->credit_balance_msg, $money_char.
3441 sprintf("%10.2f", -$cust_main->balance ) ];
3445 if ( $multisection ) {
3446 if ($conf->exists('svc_phone_sections')) {
3448 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3449 $total->{'total_amount'} =
3450 &$embolden_function(
3451 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3453 my $last_section = pop @sections;
3454 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3455 $total->{'total_amount'};
3456 push @sections, $last_section;
3458 push @sections, @$late_sections
3462 # make a discounts-available section, even without multisection
3463 if ( $conf->exists('discount-show_available')
3464 and my @discounts_avail = $self->_items_discounts_avail ) {
3465 my $discount_section = {
3466 'description' => $self->mt('Discounts Available'),
3471 push @sections, $discount_section;
3472 push @detail_items, map { +{
3473 'ref' => '', #should this be something else?
3474 'section' => $discount_section,
3475 'description' => &$escape_function( $_->{description} ),
3476 'amount' => $money_char . &$escape_function( $_->{amount} ),
3477 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3478 } } @discounts_avail;
3481 # All sections and items are built; now fill in templates.
3482 my @includelist = ();
3483 push @includelist, 'summary' if $summarypage;
3484 foreach my $include ( @includelist ) {
3486 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3489 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3491 @inc_src = $conf->config($inc_file, $agentnum);
3495 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3497 my $convert_map = $convert_maps{$format}{$include};
3499 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3500 s/--\@\]/$delimiters{$format}[1]/g;
3503 &$convert_map( $conf->config($inc_file, $agentnum) );
3507 my $inc_tt = new Text::Template (
3509 SOURCE => [ map "$_\n", @inc_src ],
3510 DELIMITERS => $delimiters{$format},
3511 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3513 unless ( $inc_tt->compile() ) {
3514 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3515 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3519 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3521 $invoice_data{$include} =~ s/\n+$//
3522 if ($format eq 'latex');
3527 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3528 /invoice_lines\((\d*)\)/;
3529 $invoice_lines += $1 || scalar(@buf);
3532 die "no invoice_lines() functions in template?"
3533 if ( $format eq 'template' && !$wasfunc );
3535 if ($format eq 'template') {
3537 if ( $invoice_lines ) {
3538 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3539 $invoice_data{'total_pages'}++
3540 if scalar(@buf) % $invoice_lines;
3543 #setup subroutine for the template
3544 $invoice_data{invoice_lines} = sub {
3545 my $lines = shift || scalar(@buf);
3557 push @collect, split("\n",
3558 $text_template->fill_in( HASH => \%invoice_data )
3560 $invoice_data{'page'}++;
3562 map "$_\n", @collect;
3564 # this is where we actually create the invoice
3565 warn "filling in template for invoice ". $self->invnum. "\n"
3567 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3570 $text_template->fill_in(HASH => \%invoice_data);
3574 # helper routine for generating date ranges
3575 sub _prior_month30s {
3578 [ 1, 2592000 ], # 0-30 days ago
3579 [ 2592000, 5184000 ], # 30-60 days ago
3580 [ 5184000, 7776000 ], # 60-90 days ago
3581 [ 7776000, 0 ], # 90+ days ago
3584 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3585 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3590 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3592 Returns an postscript invoice, as a scalar.
3594 Options can be passed as a hashref (recommended) or as a list of time, template
3595 and then any key/value pairs for any other options.
3597 I<time> an optional value used to control the printing of overdue messages. The
3598 default is now. It isn't the date of the invoice; that's the `_date' field.
3599 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3600 L<Time::Local> and L<Date::Parse> for conversion functions.
3602 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3609 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3610 my $ps = generate_ps($file);
3612 unlink($barcodefile) if $barcodefile;
3617 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3619 Returns an PDF invoice, as a scalar.
3621 Options can be passed as a hashref (recommended) or as a list of time, template
3622 and then any key/value pairs for any other options.
3624 I<time> an optional value used to control the printing of overdue messages. The
3625 default is now. It isn't the date of the invoice; that's the `_date' field.
3626 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3627 L<Time::Local> and L<Date::Parse> for conversion functions.
3629 I<template>, if specified, is the name of a suffix for alternate invoices.
3631 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3638 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3639 my $pdf = generate_pdf($file);
3641 unlink($barcodefile) if $barcodefile;
3646 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3648 Returns an HTML invoice, as a scalar.
3650 I<time> an optional value used to control the printing of overdue messages. The
3651 default is now. It isn't the date of the invoice; that's the `_date' field.
3652 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3653 L<Time::Local> and L<Date::Parse> for conversion functions.
3655 I<template>, if specified, is the name of a suffix for alternate invoices.
3657 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3659 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3660 when emailing the invoice as part of a multipart/related MIME email.
3668 %params = %{ shift() };
3670 $params{'time'} = shift;
3671 $params{'template'} = shift;
3672 $params{'cid'} = shift;
3675 $params{'format'} = 'html';
3677 $self->print_generic( %params );
3680 # quick subroutine for print_latex
3682 # There are ten characters that LaTeX treats as special characters, which
3683 # means that they do not simply typeset themselves:
3684 # # $ % & ~ _ ^ \ { }
3686 # TeX ignores blanks following an escaped character; if you want a blank (as
3687 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3691 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3692 $value =~ s/([<>])/\$$1\$/g;
3698 encode_entities($value);
3702 sub _html_escape_nbsp {
3703 my $value = _html_escape(shift);
3704 $value =~ s/ +/ /g;
3708 #utility methods for print_*
3710 sub _translate_old_latex_format {
3711 warn "_translate_old_latex_format called\n"
3718 if ( $line =~ /^%%Detail\s*$/ ) {
3720 push @template, q![@--!,
3721 q! foreach my $_tr_line (@detail_items) {!,
3722 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3723 q! $_tr_line->{'description'} .= !,
3724 q! "\\tabularnewline\n~~".!,
3725 q! join( "\\tabularnewline\n~~",!,
3726 q! @{$_tr_line->{'ext_description'}}!,
3730 while ( ( my $line_item_line = shift )
3731 !~ /^%%EndDetail\s*$/ ) {
3732 $line_item_line =~ s/'/\\'/g; # nice LTS
3733 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3734 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3735 push @template, " \$OUT .= '$line_item_line';";
3738 push @template, '}',
3741 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3743 push @template, '[@--',
3744 ' foreach my $_tr_line (@total_items) {';
3746 while ( ( my $total_item_line = shift )
3747 !~ /^%%EndTotalDetails\s*$/ ) {
3748 $total_item_line =~ s/'/\\'/g; # nice LTS
3749 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3750 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3751 push @template, " \$OUT .= '$total_item_line';";
3754 push @template, '}',
3758 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3759 push @template, $line;
3765 warn "$_\n" foreach @template;
3773 my $conf = $self->conf;
3775 #check for an invoice-specific override
3776 return $self->invoice_terms if $self->invoice_terms;
3778 #check for a customer- specific override
3779 my $cust_main = $self->cust_main;
3780 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3782 #use configured default
3783 $conf->config('invoice_default_terms') || '';
3789 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3790 $duedate = $self->_date() + ( $1 * 86400 );
3797 $self->due_date ? time2str(shift, $self->due_date) : '';
3800 sub balance_due_msg {
3802 my $msg = $self->mt('Balance Due');
3803 return $msg unless $self->terms;
3804 if ( $self->due_date ) {
3805 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3806 $self->due_date2str($date_format);
3807 } elsif ( $self->terms ) {
3808 $msg .= ' - '. $self->terms;
3813 sub balance_due_date {
3815 my $conf = $self->conf;
3817 if ( $conf->exists('invoice_default_terms')
3818 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3819 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3824 sub credit_balance_msg {
3826 $self->mt('Credit Balance Remaining')
3829 =item invnum_date_pretty
3831 Returns a string with the invoice number and date, for example:
3832 "Invoice #54 (3/20/2008)"
3836 sub invnum_date_pretty {
3838 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3843 Returns a string with the date, for example: "3/20/2008"
3849 time2str($date_format, $self->_date);
3852 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3854 Generate section information for all items appearing on this invoice.
3855 This will only be called for multi-section invoices.
3857 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3858 related display records (L<FS::cust_bill_pkg_display>) and organize
3859 them into two groups ("early" and "late" according to whether they come
3860 before or after the total), then into sections. A subtotal is calculated
3863 Section descriptions are returned in sort weight order. Each consists
3864 of a hash containing:
3866 description: the package category name, escaped
3867 subtotal: the total charges in that section
3868 tax_section: a flag indicating that the section contains only tax charges
3869 summarized: same as tax_section, for some reason
3870 sort_weight: the package category's sort weight
3872 If 'condense' is set on the display record, it also contains everything
3873 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3874 coderefs to generate parts of the invoice. This is not advised.
3878 LATE: an arrayref to push the "late" section hashes onto. The "early"
3879 group is simply returned from the method.
3881 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3882 Turning this on has the following effects:
3883 - Ignores display items with the 'summary' flag.
3884 - Combines all items into the "early" group.
3885 - Creates sections for all non-disabled package categories, even if they
3886 have no charges on this invoice, as well as a section with no name.
3888 ESCAPE: an escape function to use for section titles.
3890 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3891 sorted list. If there are any of these, section subtotals exclude
3894 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3895 passed through to C<_condense_section()>.
3899 use vars qw(%pkg_category_cache);
3900 sub _items_sections {
3903 my $summarypage = shift;
3905 my $extra_sections = shift;
3909 my %late_subtotal = ();
3912 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3915 my $usage = $cust_bill_pkg->usage;
3917 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3918 next if ( $display->summary && $summarypage );
3920 my $section = $display->section;
3921 my $type = $display->type;
3923 $not_tax{$section} = 1
3924 unless $cust_bill_pkg->pkgnum == 0;
3926 if ( $display->post_total && !$summarypage ) {
3927 if (! $type || $type eq 'S') {
3928 $late_subtotal{$section} += $cust_bill_pkg->setup
3929 if $cust_bill_pkg->setup != 0
3930 || $cust_bill_pkg->setup_show_zero;
3934 $late_subtotal{$section} += $cust_bill_pkg->recur
3935 if $cust_bill_pkg->recur != 0
3936 || $cust_bill_pkg->recur_show_zero;
3939 if ($type && $type eq 'R') {
3940 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3941 if $cust_bill_pkg->recur != 0
3942 || $cust_bill_pkg->recur_show_zero;
3945 if ($type && $type eq 'U') {
3946 $late_subtotal{$section} += $usage
3947 unless scalar(@$extra_sections);
3952 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3954 if (! $type || $type eq 'S') {
3955 $subtotal{$section} += $cust_bill_pkg->setup
3956 if $cust_bill_pkg->setup != 0
3957 || $cust_bill_pkg->setup_show_zero;
3961 $subtotal{$section} += $cust_bill_pkg->recur
3962 if $cust_bill_pkg->recur != 0
3963 || $cust_bill_pkg->recur_show_zero;
3966 if ($type && $type eq 'R') {
3967 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3968 if $cust_bill_pkg->recur != 0
3969 || $cust_bill_pkg->recur_show_zero;
3972 if ($type && $type eq 'U') {
3973 $subtotal{$section} += $usage
3974 unless scalar(@$extra_sections);
3983 %pkg_category_cache = ();
3985 push @$late, map { { 'description' => &{$escape}($_),
3986 'subtotal' => $late_subtotal{$_},
3988 'sort_weight' => ( _pkg_category($_)
3989 ? _pkg_category($_)->weight
3992 ((_pkg_category($_) && _pkg_category($_)->condense)
3993 ? $self->_condense_section($format)
3997 sort _sectionsort keys %late_subtotal;
4000 if ( $summarypage ) {
4001 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
4002 map { $_->categoryname } qsearch('pkg_category', {});
4003 push @sections, '' if exists($subtotal{''});
4005 @sections = keys %subtotal;
4008 my @early = map { { 'description' => &{$escape}($_),
4009 'subtotal' => $subtotal{$_},
4010 'summarized' => $not_tax{$_} ? '' : 'Y',
4011 'tax_section' => $not_tax{$_} ? '' : 'Y',
4012 'sort_weight' => ( _pkg_category($_)
4013 ? _pkg_category($_)->weight
4016 ((_pkg_category($_) && _pkg_category($_)->condense)
4017 ? $self->_condense_section($format)
4022 push @early, @$extra_sections if $extra_sections;
4024 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
4028 #helper subs for above
4031 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
4035 my $categoryname = shift;
4036 $pkg_category_cache{$categoryname} ||=
4037 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
4040 my %condensed_format = (
4041 'label' => [ qw( Description Qty Amount ) ],
4043 sub { shift->{description} },
4044 sub { shift->{quantity} },
4045 sub { my($href, %opt) = @_;
4046 ($opt{dollar} || ''). $href->{amount};
4049 'align' => [ qw( l r r ) ],
4050 'span' => [ qw( 5 1 1 ) ], # unitprices?
4051 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
4054 sub _condense_section {
4055 my ( $self, $format ) = ( shift, shift );
4057 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
4058 qw( description_generator
4061 total_line_generator
4066 sub _condensed_generator_defaults {
4067 my ( $self, $format ) = ( shift, shift );
4068 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
4077 sub _condensed_header_generator {
4078 my ( $self, $format ) = ( shift, shift );
4080 my ( $f, $prefix, $suffix, $separator, $column ) =
4081 _condensed_generator_defaults($format);
4083 if ($format eq 'latex') {
4084 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
4085 $suffix = "\\\\\n\\hline";
4088 sub { my ($d,$a,$s,$w) = @_;
4089 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4091 } elsif ( $format eq 'html' ) {
4092 $prefix = '<th></th>';
4096 sub { my ($d,$a,$s,$w) = @_;
4097 return qq!<th align="$html_align{$a}">$d</th>!;
4105 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4107 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4110 $prefix. join($separator, @result). $suffix;
4115 sub _condensed_description_generator {
4116 my ( $self, $format ) = ( shift, shift );
4118 my ( $f, $prefix, $suffix, $separator, $column ) =
4119 _condensed_generator_defaults($format);
4121 my $money_char = '$';
4122 if ($format eq 'latex') {
4123 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4125 $separator = " & \n";
4127 sub { my ($d,$a,$s,$w) = @_;
4128 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4130 $money_char = '\\dollar';
4131 }elsif ( $format eq 'html' ) {
4132 $prefix = '"><td align="center"></td>';
4136 sub { my ($d,$a,$s,$w) = @_;
4137 return qq!<td align="$html_align{$a}">$d</td>!;
4139 #$money_char = $conf->config('money_char') || '$';
4140 $money_char = ''; # this is madness
4148 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4150 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4152 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4153 map { $f->{$_}->[$i] } qw(align span width)
4157 $prefix. join( $separator, @result ). $suffix;
4162 sub _condensed_total_generator {
4163 my ( $self, $format ) = ( shift, shift );
4165 my ( $f, $prefix, $suffix, $separator, $column ) =
4166 _condensed_generator_defaults($format);
4169 if ($format eq 'latex') {
4172 $separator = " & \n";
4174 sub { my ($d,$a,$s,$w) = @_;
4175 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4177 }elsif ( $format eq 'html' ) {
4181 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4183 sub { my ($d,$a,$s,$w) = @_;
4184 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4193 # my $r = &{$f->{fields}->[$i]}(@args);
4194 # $r .= ' Total' unless $i;
4196 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4198 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4199 map { $f->{$_}->[$i] } qw(align span width)
4203 $prefix. join( $separator, @result ). $suffix;
4208 =item total_line_generator FORMAT
4210 Returns a coderef used for generation of invoice total line items for this
4211 usage_class. FORMAT is either html or latex
4215 # should not be used: will have issues with hash element names (description vs
4216 # total_item and amount vs total_amount -- another array of functions?
4218 sub _condensed_total_line_generator {
4219 my ( $self, $format ) = ( shift, shift );
4221 my ( $f, $prefix, $suffix, $separator, $column ) =
4222 _condensed_generator_defaults($format);
4225 if ($format eq 'latex') {
4228 $separator = " & \n";
4230 sub { my ($d,$a,$s,$w) = @_;
4231 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4233 }elsif ( $format eq 'html' ) {
4237 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4239 sub { my ($d,$a,$s,$w) = @_;
4240 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4249 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4251 &{$column}( &{$f->{fields}->[$i]}(@args),
4252 map { $f->{$_}->[$i] } qw(align span width)
4256 $prefix. join( $separator, @result ). $suffix;
4261 #sub _items_extra_usage_sections {
4263 # my $escape = shift;
4265 # my %sections = ();
4267 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4268 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4270 # next unless $cust_bill_pkg->pkgnum > 0;
4272 # foreach my $section ( keys %usage_class ) {
4274 # my $usage = $cust_bill_pkg->usage($section);
4276 # next unless $usage && $usage > 0;
4278 # $sections{$section} ||= 0;
4279 # $sections{$section} += $usage;
4285 # map { { 'description' => &{$escape}($_),
4286 # 'subtotal' => $sections{$_},
4287 # 'summarized' => '',
4288 # 'tax_section' => '',
4291 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4295 sub _items_extra_usage_sections {
4297 my $conf = $self->conf;
4305 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4307 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4308 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4309 next unless $cust_bill_pkg->pkgnum > 0;
4311 foreach my $classnum ( keys %usage_class ) {
4312 my $section = $usage_class{$classnum}->classname;
4313 $classnums{$section} = $classnum;
4315 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4316 my $amount = $detail->amount;
4317 next unless $amount && $amount > 0;
4319 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4320 $sections{$section}{amount} += $amount; #subtotal
4321 $sections{$section}{calls}++;
4322 $sections{$section}{duration} += $detail->duration;
4324 my $desc = $detail->regionname;
4325 my $description = $desc;
4326 $description = substr($desc, 0, $maxlength). '...'
4327 if $format eq 'latex' && length($desc) > $maxlength;
4329 $lines{$section}{$desc} ||= {
4330 description => &{$escape}($description),
4331 #pkgpart => $part_pkg->pkgpart,
4332 pkgnum => $cust_bill_pkg->pkgnum,
4337 #unit_amount => $cust_bill_pkg->unitrecur,
4338 quantity => $cust_bill_pkg->quantity,
4339 product_code => 'N/A',
4340 ext_description => [],
4343 $lines{$section}{$desc}{amount} += $amount;
4344 $lines{$section}{$desc}{calls}++;
4345 $lines{$section}{$desc}{duration} += $detail->duration;
4351 my %sectionmap = ();
4352 foreach (keys %sections) {
4353 my $usage_class = $usage_class{$classnums{$_}};
4354 $sectionmap{$_} = { 'description' => &{$escape}($_),
4355 'amount' => $sections{$_}{amount}, #subtotal
4356 'calls' => $sections{$_}{calls},
4357 'duration' => $sections{$_}{duration},
4359 'tax_section' => '',
4360 'sort_weight' => $usage_class->weight,
4361 ( $usage_class->format
4362 ? ( map { $_ => $usage_class->$_($format) }
4363 qw( description_generator header_generator total_generator total_line_generator )
4370 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4374 foreach my $section ( keys %lines ) {
4375 foreach my $line ( keys %{$lines{$section}} ) {
4376 my $l = $lines{$section}{$line};
4377 $l->{section} = $sectionmap{$section};
4378 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4379 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4384 return(\@sections, \@lines);
4390 my $end = $self->_date;
4392 # start at date of previous invoice + 1 second or 0 if no previous invoice
4393 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4394 $start = 0 if !$start;
4397 my $cust_main = $self->cust_main;
4398 my @pkgs = $cust_main->all_pkgs;
4399 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4402 foreach my $pkg ( @pkgs ) {
4403 my @h_cust_svc = $pkg->h_cust_svc($end);
4404 foreach my $h_cust_svc ( @h_cust_svc ) {
4405 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4406 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4408 my $inserted = $h_cust_svc->date_inserted;
4409 my $deleted = $h_cust_svc->date_deleted;
4410 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4412 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4414 # DID either activated or ported in; cannot be both for same DID simultaneously
4415 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4416 && (!$phone_inserted->lnp_status
4417 || $phone_inserted->lnp_status eq ''
4418 || $phone_inserted->lnp_status eq 'native')) {
4421 else { # this one not so clean, should probably move to (h_)svc_phone
4422 my $phone_portedin = qsearchs( 'h_svc_phone',
4423 { 'svcnum' => $h_cust_svc->svcnum,
4424 'lnp_status' => 'portedin' },
4425 FS::h_svc_phone->sql_h_searchs($end),
4427 $num_portedin++ if $phone_portedin;
4430 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4431 if($deleted >= $start && $deleted <= $end && $phone_deleted
4432 && (!$phone_deleted->lnp_status
4433 || $phone_deleted->lnp_status ne 'portingout')) {
4436 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4437 && $phone_deleted->lnp_status
4438 && $phone_deleted->lnp_status eq 'portingout') {
4442 # increment usage minutes
4443 if ( $phone_inserted ) {
4444 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4445 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4448 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4451 # don't look at this service again
4452 push @seen, $h_cust_svc->svcnum;
4456 $minutes = sprintf("%d", $minutes);
4457 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4458 . "$num_deactivated Ported-Out: $num_portedout ",
4459 "Total Minutes: $minutes");
4462 sub _items_accountcode_cdr {
4467 my $section = { 'amount' => 0,
4470 'sort_weight' => '',
4472 'description' => 'Usage by Account Code',
4478 my %accountcodes = ();
4480 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4481 next unless $cust_bill_pkg->pkgnum > 0;
4483 my @header = $cust_bill_pkg->details_header;
4484 next unless scalar(@header);
4485 $section->{'header'} = join(',',@header);
4487 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4489 $section->{'header'} = $detail->formatted('format' => $format)
4490 if($detail->detail eq $section->{'header'});
4492 my $accountcode = $detail->accountcode;
4493 next unless $accountcode;
4495 my $amount = $detail->amount;
4496 next unless $amount && $amount > 0;
4498 $accountcodes{$accountcode} ||= {
4499 description => $accountcode,
4506 product_code => 'N/A',
4507 section => $section,
4508 ext_description => [ $section->{'header'} ],
4512 $section->{'amount'} += $amount;
4513 $accountcodes{$accountcode}{'amount'} += $amount;
4514 $accountcodes{$accountcode}{calls}++;
4515 $accountcodes{$accountcode}{duration} += $detail->duration;
4516 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4520 foreach my $l ( values %accountcodes ) {
4521 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4522 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4523 foreach my $sorted_detail ( @sorted_detail ) {
4524 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4526 delete $l->{detail_temp};
4530 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4532 return ($section,\@sorted_lines);
4535 sub _items_svc_phone_sections {
4537 my $conf = $self->conf;
4545 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4547 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4548 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4550 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4551 next unless $cust_bill_pkg->pkgnum > 0;
4553 my @header = $cust_bill_pkg->details_header;
4554 next unless scalar(@header);
4556 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4558 my $phonenum = $detail->phonenum;
4559 next unless $phonenum;
4561 my $amount = $detail->amount;
4562 next unless $amount && $amount > 0;
4564 $sections{$phonenum} ||= { 'amount' => 0,
4567 'sort_weight' => -1,
4568 'phonenum' => $phonenum,
4570 $sections{$phonenum}{amount} += $amount; #subtotal
4571 $sections{$phonenum}{calls}++;
4572 $sections{$phonenum}{duration} += $detail->duration;
4574 my $desc = $detail->regionname;
4575 my $description = $desc;
4576 $description = substr($desc, 0, $maxlength). '...'
4577 if $format eq 'latex' && length($desc) > $maxlength;
4579 $lines{$phonenum}{$desc} ||= {
4580 description => &{$escape}($description),
4581 #pkgpart => $part_pkg->pkgpart,
4589 product_code => 'N/A',
4590 ext_description => [],
4593 $lines{$phonenum}{$desc}{amount} += $amount;
4594 $lines{$phonenum}{$desc}{calls}++;
4595 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4597 my $line = $usage_class{$detail->classnum}->classname;
4598 $sections{"$phonenum $line"} ||=
4602 'sort_weight' => $usage_class{$detail->classnum}->weight,
4603 'phonenum' => $phonenum,
4604 'header' => [ @header ],
4606 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4607 $sections{"$phonenum $line"}{calls}++;
4608 $sections{"$phonenum $line"}{duration} += $detail->duration;
4610 $lines{"$phonenum $line"}{$desc} ||= {
4611 description => &{$escape}($description),
4612 #pkgpart => $part_pkg->pkgpart,
4620 product_code => 'N/A',
4621 ext_description => [],
4624 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4625 $lines{"$phonenum $line"}{$desc}{calls}++;
4626 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4627 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4628 $detail->formatted('format' => $format);
4633 my %sectionmap = ();
4634 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4635 foreach ( keys %sections ) {
4636 my @header = @{ $sections{$_}{header} || [] };
4638 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4639 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4640 my $usage_class = $summary ? $simple : $usage_simple;
4641 my $ending = $summary ? ' usage charges' : '';
4644 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4646 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4647 'amount' => $sections{$_}{amount}, #subtotal
4648 'calls' => $sections{$_}{calls},
4649 'duration' => $sections{$_}{duration},
4651 'tax_section' => '',
4652 'phonenum' => $sections{$_}{phonenum},
4653 'sort_weight' => $sections{$_}{sort_weight},
4654 'post_total' => $summary, #inspire pagebreak
4656 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4657 qw( description_generator
4660 total_line_generator
4667 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4668 $a->{sort_weight} <=> $b->{sort_weight}
4673 foreach my $section ( keys %lines ) {
4674 foreach my $line ( keys %{$lines{$section}} ) {
4675 my $l = $lines{$section}{$line};
4676 $l->{section} = $sectionmap{$section};
4677 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4678 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4683 if($conf->exists('phone_usage_class_summary')) {
4684 # this only works with Latex
4688 # after this, we'll have only two sections per DID:
4689 # Calls Summary and Calls Detail
4690 foreach my $section ( @sections ) {
4691 if($section->{'post_total'}) {
4692 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4693 $section->{'total_line_generator'} = sub { '' };
4694 $section->{'total_generator'} = sub { '' };
4695 $section->{'header_generator'} = sub { '' };
4696 $section->{'description_generator'} = '';
4697 push @newsections, $section;
4698 my %calls_detail = %$section;
4699 $calls_detail{'post_total'} = '';
4700 $calls_detail{'sort_weight'} = '';
4701 $calls_detail{'description_generator'} = sub { '' };
4702 $calls_detail{'header_generator'} = sub {
4703 return ' & Date/Time & Called Number & Duration & Price'
4704 if $format eq 'latex';
4707 $calls_detail{'description'} = 'Calls Detail: '
4708 . $section->{'phonenum'};
4709 push @newsections, \%calls_detail;
4713 # after this, each usage class is collapsed/summarized into a single
4714 # line under the Calls Summary section
4715 foreach my $newsection ( @newsections ) {
4716 if($newsection->{'post_total'}) { # this means Calls Summary
4717 foreach my $section ( @sections ) {
4718 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4719 && !$section->{'post_total'});
4720 my $newdesc = $section->{'description'};
4721 my $tn = $section->{'phonenum'};
4722 $newdesc =~ s/$tn//g;
4723 my $line = { ext_description => [],
4727 calls => $section->{'calls'},
4728 section => $newsection,
4729 duration => $section->{'duration'},
4730 description => $newdesc,
4731 amount => sprintf("%.2f",$section->{'amount'}),
4732 product_code => 'N/A',
4734 push @newlines, $line;
4739 # after this, Calls Details is populated with all CDRs
4740 foreach my $newsection ( @newsections ) {
4741 if(!$newsection->{'post_total'}) { # this means Calls Details
4742 foreach my $line ( @lines ) {
4743 next unless (scalar(@{$line->{'ext_description'}}) &&
4744 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4746 my @extdesc = @{$line->{'ext_description'}};
4748 foreach my $extdesc ( @extdesc ) {
4749 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4750 push @newextdesc, $extdesc;
4752 $line->{'ext_description'} = \@newextdesc;
4753 $line->{'section'} = $newsection;
4754 push @newlines, $line;
4759 return(\@newsections, \@newlines);
4762 return(\@sections, \@lines);
4766 sub _items { # seems to be unused
4769 #my @display = scalar(@_)
4771 # : qw( _items_previous _items_pkg );
4772 # #: qw( _items_pkg );
4773 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4774 my @display = qw( _items_previous _items_pkg );
4777 foreach my $display ( @display ) {
4778 push @b, $self->$display(@_);
4783 sub _items_previous {
4785 my $conf = $self->conf;
4786 my $cust_main = $self->cust_main;
4787 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4789 foreach ( @pr_cust_bill ) {
4790 my $date = $conf->exists('invoice_show_prior_due_date')
4791 ? 'due '. $_->due_date2str($date_format)
4792 : time2str($date_format, $_->_date);
4794 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4795 #'pkgpart' => 'N/A',
4797 'amount' => sprintf("%.2f", $_->owed),
4803 # 'description' => 'Previous Balance',
4804 # #'pkgpart' => 'N/A',
4805 # 'pkgnum' => 'N/A',
4806 # 'amount' => sprintf("%10.2f", $pr_total ),
4807 # 'ext_description' => [ map {
4808 # "Invoice ". $_->invnum.
4809 # " (". time2str("%x",$_->_date). ") ".
4810 # sprintf("%10.2f", $_->owed)
4811 # } @pr_cust_bill ],
4816 =item _items_pkg [ OPTIONS ]
4818 Return line item hashes for each package item on this invoice. Nearly
4821 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4823 The only OPTIONS accepted is 'section', which may point to a hashref
4824 with a key named 'condensed', which may have a true value. If it
4825 does, this method tries to merge identical items into items with
4826 'quantity' equal to the number of items (not the sum of their
4827 separate quantities, for some reason).
4835 warn "$me _items_pkg searching for all package line items\n"
4838 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4840 warn "$me _items_pkg filtering line items\n"
4842 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4844 if ($options{section} && $options{section}->{condensed}) {
4846 warn "$me _items_pkg condensing section\n"
4850 local $Storable::canonical = 1;
4851 foreach ( @items ) {
4853 delete $item->{ref};
4854 delete $item->{ext_description};
4855 my $key = freeze($item);
4856 $itemshash{$key} ||= 0;
4857 $itemshash{$key} ++; # += $item->{quantity};
4859 @items = sort { $a->{description} cmp $b->{description} }
4860 map { my $i = thaw($_);
4861 $i->{quantity} = $itemshash{$_};
4863 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4869 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4876 return 0 unless $a->itemdesc cmp $b->itemdesc;
4877 return -1 if $b->itemdesc eq 'Tax';
4878 return 1 if $a->itemdesc eq 'Tax';
4879 return -1 if $b->itemdesc eq 'Other surcharges';
4880 return 1 if $a->itemdesc eq 'Other surcharges';
4881 $a->itemdesc cmp $b->itemdesc;
4886 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4887 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4890 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4892 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4893 list of hashrefs describing the line items they generate on the invoice.
4895 OPTIONS may include:
4897 format: the invoice format.
4899 escape_function: the function used to escape strings.
4901 DEPRECATED? (expensive, mostly unused?)
4902 format_function: the function used to format CDRs.
4904 section: a hashref containing 'description'; if this is present,
4905 cust_bill_pkg_display records not belonging to this section are
4908 multisection: a flag indicating that this is a multisection invoice,
4909 which does something complicated.
4911 multilocation: a flag to display the location label for the package.
4913 Returns a list of hashrefs, each of which may contain:
4915 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4916 ext_description, which is an arrayref of detail lines to show below
4921 sub _items_cust_bill_pkg {
4923 my $conf = $self->conf;
4924 my $cust_bill_pkgs = shift;
4927 my $format = $opt{format} || '';
4928 my $escape_function = $opt{escape_function} || sub { shift };
4929 my $format_function = $opt{format_function} || '';
4930 my $no_usage = $opt{no_usage} || '';
4931 my $unsquelched = $opt{unsquelched} || ''; #unused
4932 my $section = $opt{section}->{description} if $opt{section};
4933 my $summary_page = $opt{summary_page} || ''; #unused
4934 my $multilocation = $opt{multilocation} || '';
4935 my $multisection = $opt{multisection} || '';
4936 my $discount_show_always = 0;
4938 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4940 my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
4943 my ($s, $r, $u) = ( undef, undef, undef );
4944 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4947 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4948 if ( $_ && !$cust_bill_pkg->hidden ) {
4949 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4950 $_->{amount} =~ s/^\-0\.00$/0.00/;
4951 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4953 if $_->{amount} != 0
4954 || $discount_show_always
4955 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4956 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4962 my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
4964 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4965 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4968 foreach my $display ( grep { defined($section)
4969 ? $_->section eq $section
4972 #grep { !$_->summary || !$summary_page } # bunk!
4973 grep { !$_->summary || $multisection }
4974 @cust_bill_pkg_display
4978 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4979 $display->billpkgdisplaynum. "\n"
4982 my $type = $display->type;
4984 my $desc = $cust_bill_pkg->desc;
4985 $desc = substr($desc, 0, $maxlength). '...'
4986 if $format eq 'latex' && length($desc) > $maxlength;
4988 my %details_opt = ( 'format' => $format,
4989 'escape_function' => $escape_function,
4990 'format_function' => $format_function,
4991 'no_usage' => $opt{'no_usage'},
4994 if ( $cust_bill_pkg->pkgnum > 0 ) {
4996 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4999 my $cust_pkg = $cust_bill_pkg->cust_pkg;
5001 # which pkgpart to show for display purposes?
5002 my $pkgpart = $cust_bill_pkg->pkgpart_override || $cust_pkg->pkgpart;
5004 # start/end dates for invoice formats that do nonstandard
5006 my %item_dates = ();
5007 %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate')
5008 unless $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
5010 if ( (!$type || $type eq 'S')
5011 && ( $cust_bill_pkg->setup != 0
5012 || $cust_bill_pkg->setup_show_zero
5017 warn "$me _items_cust_bill_pkg adding setup\n"
5020 my $description = $desc;
5021 $description .= ' Setup'
5022 if $cust_bill_pkg->recur != 0
5023 || $discount_show_always
5024 || $cust_bill_pkg->recur_show_zero;
5028 unless ( $cust_pkg->part_pkg->hide_svc_detail
5029 || $cust_bill_pkg->hidden )
5032 my @svc_labels = map &{$escape_function}($_),
5033 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5034 push @d, @svc_labels
5035 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5036 $svc_label = $svc_labels[0];
5038 if ( $multilocation ) {
5039 my $loc = $cust_pkg->location_label;
5040 $loc = substr($loc, 0, $maxlength). '...'
5041 if $format eq 'latex' && length($loc) > $maxlength;
5042 push @d, &{$escape_function}($loc);
5045 } #unless hiding service details
5047 push @d, $cust_bill_pkg->details(%details_opt)
5048 if $cust_bill_pkg->recur == 0;
5050 if ( $cust_bill_pkg->hidden ) {
5051 $s->{amount} += $cust_bill_pkg->setup;
5052 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
5053 push @{ $s->{ext_description} }, @d;
5057 description => $description,
5058 pkgpart => $pkgpart,
5059 pkgnum => $cust_bill_pkg->pkgnum,
5060 amount => $cust_bill_pkg->setup,
5061 setup_show_zero => $cust_bill_pkg->setup_show_zero,
5062 unit_amount => $cust_bill_pkg->unitsetup,
5063 quantity => $cust_bill_pkg->quantity,
5064 ext_description => \@d,
5065 svc_label => ($svc_label || ''),
5071 if ( ( !$type || $type eq 'R' || $type eq 'U' )
5073 $cust_bill_pkg->recur != 0
5074 || $cust_bill_pkg->setup == 0
5075 || $discount_show_always
5076 || $cust_bill_pkg->recur_show_zero
5081 warn "$me _items_cust_bill_pkg adding recur/usage\n"
5084 my $is_summary = $display->summary;
5085 my $description = ($is_summary && $type && $type eq 'U')
5086 ? "Usage charges" : $desc;
5088 my $part_pkg = $cust_pkg->part_pkg;
5090 #pry be a bit more efficient to look some of this conf stuff up
5093 $conf->exists('disable_line_item_date_ranges')
5094 || $part_pkg->option('disable_line_item_date_ranges',1)
5095 || ! $cust_bill_pkg->sdate
5096 || ! $cust_bill_pkg->edate
5099 my $date_style = '';
5100 $date_style = $conf->config( 'cust_bill-line_item-date_style-non_monthly',
5101 $cust_main->agentnum
5103 if $part_pkg && $part_pkg->freq !~ /^1m?$/;
5104 $date_style ||= $conf->config( 'cust_bill-line_item-date_style',
5105 $cust_main->agentnum
5107 if ( defined($date_style) && $date_style eq 'month_of' ) {
5108 $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
5109 } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
5110 my $desc = $conf->config( 'cust_bill-line_item-date_description',
5111 $cust_main->agentnum
5113 $desc .= ' ' unless $desc =~ /\s$/;
5114 $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
5116 $time_period = time2str($date_format, $cust_bill_pkg->sdate).
5117 " - ". time2str($date_format, $cust_bill_pkg->edate);
5119 $description .= " ($time_period)";
5123 my @seconds = (); # for display of usage info
5126 #at least until cust_bill_pkg has "past" ranges in addition to
5127 #the "future" sdate/edate ones... see #3032
5128 my @dates = ( $self->_date );
5129 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
5130 push @dates, $prev->sdate if $prev;
5131 push @dates, undef if !$prev;
5133 unless ( $cust_pkg->part_pkg->hide_svc_detail
5134 || $cust_bill_pkg->itemdesc
5135 || $cust_bill_pkg->hidden
5136 || $is_summary && $type && $type eq 'U' )
5139 warn "$me _items_cust_bill_pkg adding service details\n"
5142 my @svc_labels = map &{$escape_function}($_),
5143 $cust_pkg->h_labels_short($self->_date, undef, 'I');
5144 push @d, @svc_labels
5145 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5146 $svc_label = $svc_labels[0];
5148 warn "$me _items_cust_bill_pkg done adding service details\n"
5151 if ( $multilocation ) {
5152 my $loc = $cust_pkg->location_label;
5153 $loc = substr($loc, 0, $maxlength). '...'
5154 if $format eq 'latex' && length($loc) > $maxlength;
5155 push @d, &{$escape_function}($loc);
5158 # Display of seconds_since_sqlradacct:
5159 # On the invoice, when processing @detail_items, look for a field
5160 # named 'seconds'. This will contain total seconds for each
5161 # service, in the same order as @ext_description. For services
5162 # that don't support this it will show undef.
5163 if ( $conf->exists('svc_acct-usage_seconds')
5164 and ! $cust_bill_pkg->pkgpart_override ) {
5165 foreach my $cust_svc (
5166 $cust_pkg->h_cust_svc(@dates, 'I')
5169 # eval because not having any part_export_usage exports
5170 # is a fatal error, last_bill/_date because that's how
5171 # sqlradius_hour billing does it
5173 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5175 push @seconds, $sec;
5177 } #if svc_acct-usage_seconds
5181 unless ( $is_summary ) {
5182 warn "$me _items_cust_bill_pkg adding details\n"
5185 #instead of omitting details entirely in this case (unwanted side
5186 # effects), just omit CDRs
5187 $details_opt{'no_usage'} = 1
5188 if $type && $type eq 'R';
5190 push @d, $cust_bill_pkg->details(%details_opt);
5193 warn "$me _items_cust_bill_pkg calculating amount\n"
5198 $amount = $cust_bill_pkg->recur;
5199 } elsif ($type eq 'R') {
5200 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5201 } elsif ($type eq 'U') {
5202 $amount = $cust_bill_pkg->usage;
5205 if ( !$type || $type eq 'R' ) {
5207 warn "$me _items_cust_bill_pkg adding recur\n"
5210 if ( $cust_bill_pkg->hidden ) {
5211 $r->{amount} += $amount;
5212 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5213 push @{ $r->{ext_description} }, @d;
5216 description => $description,
5217 pkgpart => $pkgpart,
5218 pkgnum => $cust_bill_pkg->pkgnum,
5220 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5221 unit_amount => $cust_bill_pkg->unitrecur,
5222 quantity => $cust_bill_pkg->quantity,
5224 ext_description => \@d,
5225 svc_label => ($svc_label || ''),
5227 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5230 } else { # $type eq 'U'
5232 warn "$me _items_cust_bill_pkg adding usage\n"
5235 if ( $cust_bill_pkg->hidden ) {
5236 $u->{amount} += $amount;
5237 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5238 push @{ $u->{ext_description} }, @d;
5241 description => $description,
5242 pkgpart => $pkgpart,
5243 pkgnum => $cust_bill_pkg->pkgnum,
5245 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5246 unit_amount => $cust_bill_pkg->unitrecur,
5247 quantity => $cust_bill_pkg->quantity,
5249 ext_description => \@d,
5254 } # recurring or usage with recurring charge
5256 } else { #pkgnum tax or one-shot line item (??)
5258 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5261 if ( $cust_bill_pkg->setup != 0 ) {
5263 'description' => $desc,
5264 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5267 if ( $cust_bill_pkg->recur != 0 ) {
5269 'description' => "$desc (".
5270 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5271 time2str($date_format, $cust_bill_pkg->edate). ')',
5272 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5280 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5281 && $conf->exists('discount-show-always'));
5285 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5287 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5288 $_->{amount} =~ s/^\-0\.00$/0.00/;
5289 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5291 if $_->{amount} != 0
5292 || $discount_show_always
5293 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5294 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5298 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5305 sub _items_credits {
5306 my( $self, %opt ) = @_;
5307 my $trim_len = $opt{'trim_len'} || 60;
5311 foreach ( $self->cust_credited ) {
5313 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5315 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5316 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5317 $reason = " ($reason) " if $reason;
5320 #'description' => 'Credit ref\#'. $_->crednum.
5321 # " (". time2str("%x",$_->cust_credit->_date) .")".
5323 'description' => $self->mt('Credit applied').' '.
5324 time2str($date_format,$_->cust_credit->_date). $reason,
5325 'amount' => sprintf("%.2f",$_->amount),
5333 sub _items_payments {
5337 #get & print payments
5338 foreach ( $self->cust_bill_pay ) {
5340 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5343 'description' => $self->mt('Payment received').' '.
5344 time2str($date_format,$_->cust_pay->_date ),
5345 'amount' => sprintf("%.2f", $_->amount )
5353 =item _items_discounts_avail
5355 Returns an array of line item hashrefs representing available term discounts
5356 for this invoice. This makes the same assumptions that apply to term
5357 discounts in general: that the package is billed monthly, at a flat rate,
5358 with no usage charges. A prorated first month will be handled, as will
5359 a setup fee if the discount is allowed to apply to setup fees.
5363 sub _items_discounts_avail {
5365 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5367 my %plans = $self->discount_plans;
5369 $list_pkgnums = grep { $_->list_pkgnums } values %plans;
5373 my $plan = $plans{$months};
5375 my $term_total = sprintf('%.2f', $plan->discounted_total);
5376 my $percent = sprintf('%.0f',
5377 100 * (1 - $term_total / $plan->base_total) );
5378 my $permonth = sprintf('%.2f', $term_total / $months);
5379 my $detail = $self->mt('discount on item'). ' '.
5380 join(', ', map { "#$_" } $plan->pkgnums)
5383 # discounts for non-integer months don't work anyway
5384 $months = sprintf("%d", $months);
5387 description => $self->mt('Save [_1]% by paying for [_2] months',
5389 amount => $self->mt('[_1] ([_2] per month)',
5390 $term_total, $money_char.$permonth),
5391 ext_description => ($detail || ''),
5394 sort { $b <=> $a } keys %plans;
5398 =item call_details [ OPTION => VALUE ... ]
5400 Returns an array of CSV strings representing the call details for this invoice
5401 The only option available is the boolean prepend_billed_number
5406 my ($self, %opt) = @_;
5408 my $format_function = sub { shift };
5410 if ($opt{prepend_billed_number}) {
5411 $format_function = sub {
5415 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5420 my @details = map { $_->details( 'format_function' => $format_function,
5421 'escape_function' => sub{ return() },
5425 $self->cust_bill_pkg;
5426 my $header = $details[0];
5427 ( $header, grep { $_ ne $header } @details );
5437 =item process_reprint
5441 sub process_reprint {
5442 process_re_X('print', @_);
5445 =item process_reemail
5449 sub process_reemail {
5450 process_re_X('email', @_);
5458 process_re_X('fax', @_);
5466 process_re_X('ftp', @_);
5473 sub process_respool {
5474 process_re_X('spool', @_);
5477 use Storable qw(thaw);
5481 my( $method, $job ) = ( shift, shift );
5482 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5484 my $param = thaw(decode_base64(shift));
5485 warn Dumper($param) if $DEBUG;
5496 my($method, $job, %param ) = @_;
5498 warn "re_X $method for job $job with param:\n".
5499 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5502 #some false laziness w/search/cust_bill.html
5504 my $orderby = 'ORDER BY cust_bill._date';
5506 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5508 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5510 my @cust_bill = qsearch( {
5511 #'select' => "cust_bill.*",
5512 'table' => 'cust_bill',
5513 'addl_from' => $addl_from,
5515 'extra_sql' => $extra_sql,
5516 'order_by' => $orderby,
5520 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5522 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5525 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5526 foreach my $cust_bill ( @cust_bill ) {
5527 $cust_bill->$method();
5529 if ( $job ) { #progressbar foo
5531 if ( time - $min_sec > $last ) {
5532 my $error = $job->update_statustext(
5533 int( 100 * $num / scalar(@cust_bill) )
5535 die $error if $error;
5546 =head1 CLASS METHODS
5552 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5557 my ($class, $start, $end) = @_;
5559 $class->paid_sql($start, $end). ' - '.
5560 $class->credited_sql($start, $end);
5565 Returns an SQL fragment to retreive the net amount (charged minus credited).
5570 my ($class, $start, $end) = @_;
5571 'charged - '. $class->credited_sql($start, $end);
5576 Returns an SQL fragment to retreive the amount paid against this invoice.
5581 my ($class, $start, $end) = @_;
5582 $start &&= "AND cust_bill_pay._date <= $start";
5583 $end &&= "AND cust_bill_pay._date > $end";
5584 $start = '' unless defined($start);
5585 $end = '' unless defined($end);
5586 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5587 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5592 Returns an SQL fragment to retreive the amount credited against this invoice.
5597 my ($class, $start, $end) = @_;
5598 $start &&= "AND cust_credit_bill._date <= $start";
5599 $end &&= "AND cust_credit_bill._date > $end";
5600 $start = '' unless defined($start);
5601 $end = '' unless defined($end);
5602 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5603 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5608 Returns an SQL fragment to retrieve the due date of an invoice.
5609 Currently only supported on PostgreSQL.
5614 my $conf = new FS::Conf;
5618 cust_bill.invoice_terms,
5619 cust_main.invoice_terms,
5620 \''.($conf->config('invoice_default_terms') || '').'\'
5621 ), E\'Net (\\\\d+)\'
5623 ) * 86400 + cust_bill._date'
5626 =item search_sql_where HASHREF
5628 Class method which returns an SQL WHERE fragment to search for parameters
5629 specified in HASHREF. Valid parameters are
5635 List reference of start date, end date, as UNIX timestamps.
5645 List reference of charged limits (exclusive).
5649 List reference of charged limits (exclusive).
5653 flag, return open invoices only
5657 flag, return net invoices only
5661 =item newest_percust
5665 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5669 sub search_sql_where {
5670 my($class, $param) = @_;
5672 warn "$me search_sql_where called with params: \n".
5673 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5679 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5680 push @search, "cust_main.agentnum = $1";
5684 if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
5685 push @search, "cust_main.refnum = $1";
5689 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5690 push @search, "cust_bill.custnum = $1";
5694 if ( $param->{'cust_classnum'} ) {
5695 my $classnums = $param->{'cust_classnum'};
5696 $classnums = [ $classnums ] if !ref($classnums);
5697 $classnums = [ grep /^\d+$/, @$classnums ];
5698 push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
5703 if ( $param->{_date} ) {
5704 my($beginning, $ending) = @{$param->{_date}};
5706 push @search, "cust_bill._date >= $beginning",
5707 "cust_bill._date < $ending";
5711 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5712 push @search, "cust_bill.invnum >= $1";
5714 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5715 push @search, "cust_bill.invnum <= $1";
5719 if ( $param->{charged} ) {
5720 my @charged = ref($param->{charged})
5721 ? @{ $param->{charged} }
5722 : ($param->{charged});
5724 push @search, map { s/^charged/cust_bill.charged/; $_; }
5728 my $owed_sql = FS::cust_bill->owed_sql;
5731 if ( $param->{owed} ) {
5732 my @owed = ref($param->{owed})
5733 ? @{ $param->{owed} }
5735 push @search, map { s/^owed/$owed_sql/; $_; }
5740 push @search, "0 != $owed_sql"
5741 if $param->{'open'};
5742 push @search, '0 != '. FS::cust_bill->net_sql
5746 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5747 if $param->{'days'};
5750 if ( $param->{'newest_percust'} ) {
5752 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5753 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5755 my @newest_where = map { my $x = $_;
5756 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5759 grep ! /^cust_main./, @search;
5760 my $newest_where = scalar(@newest_where)
5761 ? ' AND '. join(' AND ', @newest_where)
5765 push @search, "cust_bill._date = (
5766 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5767 WHERE newest_cust_bill.custnum = cust_bill.custnum
5773 #promised_date - also has an option to accept nulls
5774 if ( $param->{promised_date} ) {
5775 my($beginning, $ending, $null) = @{$param->{promised_date}};
5777 push @search, "(( cust_bill.promised_date >= $beginning AND ".
5778 "cust_bill.promised_date < $ending )" .
5779 ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
5782 #agent virtualization
5783 my $curuser = $FS::CurrentUser::CurrentUser;
5784 if ( $curuser->username eq 'fs_queue'
5785 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5787 my $newuser = qsearchs('access_user', {
5788 'username' => $username,
5792 $curuser = $newuser;
5794 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5797 push @search, $curuser->agentnums_sql;
5799 join(' AND ', @search );
5811 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5812 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base