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;
48 @ISA = qw( FS::cust_main_Mixin FS::Record );
51 $me = '[FS::cust_bill]';
53 #ask FS::UID to run this stuff for us later
54 FS::UID->install_callback( sub {
55 my $conf = new FS::Conf; #global
56 $money_char = $conf->config('money_char') || '$';
57 $date_format = $conf->config('date_format') || '%x'; #/YY
58 $rdate_format = $conf->config('date_format') || '%m/%d/%Y'; #/YYYY
59 $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
64 FS::cust_bill - Object methods for cust_bill records
70 $record = new FS::cust_bill \%hash;
71 $record = new FS::cust_bill { 'column' => 'value' };
73 $error = $record->insert;
75 $error = $new_record->replace($old_record);
77 $error = $record->delete;
79 $error = $record->check;
81 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
83 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
85 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
87 @cust_pay_objects = $cust_bill->cust_pay;
89 $tax_amount = $record->tax;
91 @lines = $cust_bill->print_text;
92 @lines = $cust_bill->print_text $time;
96 An FS::cust_bill object represents an invoice; a declaration that a customer
97 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
98 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
99 following fields are currently supported:
105 =item invnum - primary key (assigned automatically for new invoices)
107 =item custnum - customer (see L<FS::cust_main>)
109 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
110 L<Time::Local> and L<Date::Parse> for conversion functions.
112 =item charged - amount of this invoice
114 =item invoice_terms - optional terms override for this specific invoice
118 Customer info at invoice generation time
122 =item previous_balance
124 =item billing_balance
132 =item printed - deprecated
140 =item closed - books closed flag, empty or `Y'
142 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
144 =item agent_invid - legacy invoice number
154 Creates a new invoice. To add the invoice to the database, see L<"insert">.
155 Invoices are normally created by calling the bill method of a customer object
156 (see L<FS::cust_main>).
160 sub table { 'cust_bill'; }
162 sub cust_linked { $_[0]->cust_main_custnum; }
163 sub cust_unlinked_msg {
165 "WARNING: can't find cust_main.custnum ". $self->custnum.
166 ' (cust_bill.invnum '. $self->invnum. ')';
171 Adds this invoice to the database ("Posts" the invoice). If there is an error,
172 returns the error, otherwise returns false.
178 warn "$me insert called\n" if $DEBUG;
180 local $SIG{HUP} = 'IGNORE';
181 local $SIG{INT} = 'IGNORE';
182 local $SIG{QUIT} = 'IGNORE';
183 local $SIG{TERM} = 'IGNORE';
184 local $SIG{TSTP} = 'IGNORE';
185 local $SIG{PIPE} = 'IGNORE';
187 my $oldAutoCommit = $FS::UID::AutoCommit;
188 local $FS::UID::AutoCommit = 0;
191 my $error = $self->SUPER::insert;
193 $dbh->rollback if $oldAutoCommit;
197 if ( $self->get('cust_bill_pkg') ) {
198 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
199 $cust_bill_pkg->invnum($self->invnum);
200 my $error = $cust_bill_pkg->insert;
202 $dbh->rollback if $oldAutoCommit;
203 return "can't create invoice line item: $error";
208 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
215 This method now works but you probably shouldn't use it. Instead, apply a
216 credit against the invoice.
218 Using this method to delete invoices outright is really, really bad. There
219 would be no record you ever posted this invoice, and there are no check to
220 make sure charged = 0 or that there are no associated cust_bill_pkg records.
222 Really, don't use it.
228 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
230 local $SIG{HUP} = 'IGNORE';
231 local $SIG{INT} = 'IGNORE';
232 local $SIG{QUIT} = 'IGNORE';
233 local $SIG{TERM} = 'IGNORE';
234 local $SIG{TSTP} = 'IGNORE';
235 local $SIG{PIPE} = 'IGNORE';
237 my $oldAutoCommit = $FS::UID::AutoCommit;
238 local $FS::UID::AutoCommit = 0;
241 foreach my $table (qw(
253 foreach my $linked ( $self->$table() ) {
254 my $error = $linked->delete;
256 $dbh->rollback if $oldAutoCommit;
263 my $error = $self->SUPER::delete(@_);
265 $dbh->rollback if $oldAutoCommit;
269 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
275 =item replace [ OLD_RECORD ]
277 You can, but probably shouldn't modify invoices...
279 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
280 supplied, replaces this record. If there is an error, returns the error,
281 otherwise returns false.
285 #replace can be inherited from Record.pm
287 # replace_check is now the preferred way to #implement replace data checks
288 # (so $object->replace() works without an argument)
291 my( $new, $old ) = ( shift, shift );
292 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
293 #return "Can't change _date!" unless $old->_date eq $new->_date;
294 return "Can't change _date" unless $old->_date == $new->_date;
295 return "Can't change charged" unless $old->charged == $new->charged
296 || $old->charged == 0
297 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
303 =item add_cc_surcharge
309 sub add_cc_surcharge {
310 my ($self, $pkgnum, $amount) = (shift, shift, shift);
313 my $cust_bill_pkg = new FS::cust_bill_pkg({
314 'invnum' => $self->invnum,
318 $error = $cust_bill_pkg->insert;
319 return $error if $error;
321 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
322 $self->charged($self->charged+$amount);
323 $error = $self->replace;
324 return $error if $error;
326 $self->apply_payments_and_credits;
332 Checks all fields to make sure this is a valid invoice. If there is an error,
333 returns the error, otherwise returns false. Called by the insert and replace
342 $self->ut_numbern('invnum')
343 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
344 || $self->ut_numbern('_date')
345 || $self->ut_money('charged')
346 || $self->ut_numbern('printed')
347 || $self->ut_enum('closed', [ '', 'Y' ])
348 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
349 || $self->ut_numbern('agent_invid') #varchar?
351 return $error if $error;
353 $self->_date(time) unless $self->_date;
355 $self->printed(0) if $self->printed eq '';
362 Returns the displayed invoice number for this invoice: agent_invid if
363 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
369 my $conf = $self->conf;
370 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
371 return $self->agent_invid;
373 return $self->invnum;
379 Returns a list consisting of the total previous balance for this customer,
380 followed by the previous outstanding invoices (as FS::cust_bill objects also).
387 my @cust_bill = sort { $a->_date <=> $b->_date }
388 grep { $_->owed != 0 && $_->_date < $self->_date }
389 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
391 foreach ( @cust_bill ) { $total += $_->owed; }
397 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
404 { 'table' => 'cust_bill_pkg',
405 'hashref' => { 'invnum' => $self->invnum },
406 'order_by' => 'ORDER BY billpkgnum',
411 =item cust_bill_pkg_pkgnum PKGNUM
413 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
418 sub cust_bill_pkg_pkgnum {
419 my( $self, $pkgnum ) = @_;
421 { 'table' => 'cust_bill_pkg',
422 'hashref' => { 'invnum' => $self->invnum,
425 'order_by' => 'ORDER BY billpkgnum',
432 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
439 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
440 $self->cust_bill_pkg;
442 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
447 Returns true if any of the packages (or their definitions) corresponding to the
448 line items for this invoice have the no_auto flag set.
454 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
457 =item open_cust_bill_pkg
459 Returns the open line items for this invoice.
461 Note that cust_bill_pkg with both setup and recur fees are returned as two
462 separate line items, each with only one fee.
466 # modeled after cust_main::open_cust_bill
467 sub open_cust_bill_pkg {
470 # grep { $_->owed > 0 } $self->cust_bill_pkg
472 my %other = ( 'recur' => 'setup',
473 'setup' => 'recur', );
475 foreach my $field ( qw( recur setup )) {
476 push @open, map { $_->set( $other{$field}, 0 ); $_; }
477 grep { $_->owed($field) > 0 }
478 $self->cust_bill_pkg;
484 =item cust_bill_event
486 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
490 sub cust_bill_event {
492 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
495 =item num_cust_bill_event
497 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
501 sub num_cust_bill_event {
504 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
505 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
506 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
507 $sth->fetchrow_arrayref->[0];
512 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
516 #false laziness w/cust_pkg.pm
520 'table' => 'cust_event',
521 'addl_from' => 'JOIN part_event USING ( eventpart )',
522 'hashref' => { 'tablenum' => $self->invnum },
523 'extra_sql' => " AND eventtable = 'cust_bill' ",
529 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
533 #false laziness w/cust_pkg.pm
537 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
538 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
539 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
540 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
541 $sth->fetchrow_arrayref->[0];
546 Returns the customer (see L<FS::cust_main>) for this invoice.
552 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
555 =item cust_suspend_if_balance_over AMOUNT
557 Suspends the customer associated with this invoice if the total amount owed on
558 this invoice and all older invoices is greater than the specified amount.
560 Returns a list: an empty list on success or a list of errors.
564 sub cust_suspend_if_balance_over {
565 my( $self, $amount ) = ( shift, shift );
566 my $cust_main = $self->cust_main;
567 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
570 $cust_main->suspend(@_);
576 Depreciated. See the cust_credited method.
578 #Returns a list consisting of the total previous credited (see
579 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
580 #outstanding credits (FS::cust_credit objects).
586 croak "FS::cust_bill->cust_credit depreciated; see ".
587 "FS::cust_bill->cust_credit_bill";
590 #my @cust_credit = sort { $a->_date <=> $b->_date }
591 # grep { $_->credited != 0 && $_->_date < $self->_date }
592 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
594 #foreach (@cust_credit) { $total += $_->credited; }
595 #$total, @cust_credit;
600 Depreciated. See the cust_bill_pay method.
602 #Returns all payments (see L<FS::cust_pay>) for this invoice.
608 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
610 #sort { $a->_date <=> $b->_date }
611 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
617 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
620 sub cust_bill_pay_batch {
622 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
627 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
633 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
634 sort { $a->_date <=> $b->_date }
635 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
640 =item cust_credit_bill
642 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
648 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
649 sort { $a->_date <=> $b->_date }
650 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
654 sub cust_credit_bill {
655 shift->cust_credited(@_);
658 #=item cust_bill_pay_pkgnum PKGNUM
660 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
661 #with matching pkgnum.
665 #sub cust_bill_pay_pkgnum {
666 # my( $self, $pkgnum ) = @_;
667 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
668 # sort { $a->_date <=> $b->_date }
669 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
670 # 'pkgnum' => $pkgnum,
675 =item cust_bill_pay_pkg PKGNUM
677 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
678 applied against the matching pkgnum.
682 sub cust_bill_pay_pkg {
683 my( $self, $pkgnum ) = @_;
686 'select' => 'cust_bill_pay_pkg.*',
687 'table' => 'cust_bill_pay_pkg',
688 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
689 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
690 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
691 " AND cust_bill_pkg.pkgnum = $pkgnum",
696 #=item cust_credited_pkgnum PKGNUM
698 #=item cust_credit_bill_pkgnum PKGNUM
700 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
701 #with matching pkgnum.
705 #sub cust_credited_pkgnum {
706 # my( $self, $pkgnum ) = @_;
707 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
708 # sort { $a->_date <=> $b->_date }
709 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
710 # 'pkgnum' => $pkgnum,
715 #sub cust_credit_bill_pkgnum {
716 # shift->cust_credited_pkgnum(@_);
719 =item cust_credit_bill_pkg PKGNUM
721 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
722 applied against the matching pkgnum.
726 sub cust_credit_bill_pkg {
727 my( $self, $pkgnum ) = @_;
730 'select' => 'cust_credit_bill_pkg.*',
731 'table' => 'cust_credit_bill_pkg',
732 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
733 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
734 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
735 " AND cust_bill_pkg.pkgnum = $pkgnum",
740 =item cust_bill_batch
742 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
746 sub cust_bill_batch {
748 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
753 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
760 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
762 foreach (@taxlines) { $total += $_->setup; }
768 Returns the amount owed (still outstanding) on this invoice, which is charged
769 minus all payment applications (see L<FS::cust_bill_pay>) and credit
770 applications (see L<FS::cust_credit_bill>).
776 my $balance = $self->charged;
777 $balance -= $_->amount foreach ( $self->cust_bill_pay );
778 $balance -= $_->amount foreach ( $self->cust_credited );
779 $balance = sprintf( "%.2f", $balance);
780 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
785 my( $self, $pkgnum ) = @_;
787 #my $balance = $self->charged;
789 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
791 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
792 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
794 $balance = sprintf( "%.2f", $balance);
795 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
801 Returns true if this invoice should be hidden. See the
802 selfservice-hide_invoices-taxclass configuraiton setting.
808 my $conf = $self->conf;
809 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
811 my @cust_bill_pkg = $self->cust_bill_pkg;
812 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
813 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
816 =item apply_payments_and_credits [ OPTION => VALUE ... ]
818 Applies unapplied payments and credits to this invoice.
820 A hash of optional arguments may be passed. Currently "manual" is supported.
821 If true, a payment receipt is sent instead of a statement when
822 'payment_receipt_email' configuration option is set.
824 If there is an error, returns the error, otherwise returns false.
828 sub apply_payments_and_credits {
829 my( $self, %options ) = @_;
830 my $conf = $self->conf;
832 local $SIG{HUP} = 'IGNORE';
833 local $SIG{INT} = 'IGNORE';
834 local $SIG{QUIT} = 'IGNORE';
835 local $SIG{TERM} = 'IGNORE';
836 local $SIG{TSTP} = 'IGNORE';
837 local $SIG{PIPE} = 'IGNORE';
839 my $oldAutoCommit = $FS::UID::AutoCommit;
840 local $FS::UID::AutoCommit = 0;
843 $self->select_for_update; #mutex
845 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
846 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
848 if ( $conf->exists('pkg-balances') ) {
849 # limit @payments & @credits to those w/ a pkgnum grepped from $self
850 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
851 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
852 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
855 while ( $self->owed > 0 and ( @payments || @credits ) ) {
858 if ( @payments && @credits ) {
860 #decide which goes first by weight of top (unapplied) line item
862 my @open_lineitems = $self->open_cust_bill_pkg;
865 max( map { $_->part_pkg->pay_weight || 0 }
870 my $max_credit_weight =
871 max( map { $_->part_pkg->credit_weight || 0 }
877 #if both are the same... payments first? it has to be something
878 if ( $max_pay_weight >= $max_credit_weight ) {
884 } elsif ( @payments ) {
886 } elsif ( @credits ) {
889 die "guru meditation #12 and 35";
893 if ( $app eq 'pay' ) {
895 my $payment = shift @payments;
896 $unapp_amount = $payment->unapplied;
897 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
898 $app->pkgnum( $payment->pkgnum )
899 if $conf->exists('pkg-balances') && $payment->pkgnum;
901 } elsif ( $app eq 'credit' ) {
903 my $credit = shift @credits;
904 $unapp_amount = $credit->credited;
905 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
906 $app->pkgnum( $credit->pkgnum )
907 if $conf->exists('pkg-balances') && $credit->pkgnum;
910 die "guru meditation #12 and 35";
914 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
915 warn "owed_pkgnum ". $app->pkgnum;
916 $owed = $self->owed_pkgnum($app->pkgnum);
920 next unless $owed > 0;
922 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
923 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
925 $app->invnum( $self->invnum );
927 my $error = $app->insert(%options);
929 $dbh->rollback if $oldAutoCommit;
930 return "Error inserting ". $app->table. " record: $error";
932 die $error if $error;
936 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
941 =item generate_email OPTION => VALUE ...
949 sender address, required
953 alternate template name, optional
957 text attachment arrayref, optional
961 email subject, optional
965 notice name instead of "Invoice", optional
969 Returns an argument list to be passed to L<FS::Misc::send_email>.
979 my $conf = $self->conf;
981 my $me = '[FS::cust_bill::generate_email]';
984 'from' => $args{'from'},
985 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
989 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
990 'template' => $args{'template'},
991 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
992 'no_coupon' => $args{'no_coupon'},
995 my $cust_main = $self->cust_main;
997 if (ref($args{'to'}) eq 'ARRAY') {
998 $return{'to'} = $args{'to'};
1000 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
1001 $cust_main->invoicing_list
1005 if ( $conf->exists('invoice_html') ) {
1007 warn "$me creating HTML/text multipart message"
1010 $return{'nobody'} = 1;
1012 my $alternative = build MIME::Entity
1013 'Type' => 'multipart/alternative',
1014 #'Encoding' => '7bit',
1015 'Disposition' => 'inline'
1019 if ( $conf->exists('invoice_email_pdf')
1020 and scalar($conf->config('invoice_email_pdf_note')) ) {
1022 warn "$me using 'invoice_email_pdf_note' in multipart message"
1024 $data = [ map { $_ . "\n" }
1025 $conf->config('invoice_email_pdf_note')
1030 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1032 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1033 $data = $args{'print_text'};
1035 $data = [ $self->print_text(\%opt) ];
1040 $alternative->attach(
1041 'Type' => 'text/plain',
1042 'Encoding' => 'quoted-printable',
1043 #'Encoding' => '7bit',
1045 'Disposition' => 'inline',
1052 if ( $conf->exists('invoice_email_pdf')
1053 and scalar($conf->config('invoice_email_pdf_note')) ) {
1055 $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
1059 $args{'from'} =~ /\@([\w\.\-]+)/;
1060 my $from = $1 || 'example.com';
1061 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1064 my $agentnum = $cust_main->agentnum;
1065 if ( defined($args{'template'}) && length($args{'template'})
1066 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1069 $logo = 'logo_'. $args{'template'}. '.png';
1073 my $image_data = $conf->config_binary( $logo, $agentnum);
1075 $image = build MIME::Entity
1076 'Type' => 'image/png',
1077 'Encoding' => 'base64',
1078 'Data' => $image_data,
1079 'Filename' => 'logo.png',
1080 'Content-ID' => "<$content_id>",
1083 if ($conf->exists('invoice-barcode')) {
1084 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1085 $barcode = build MIME::Entity
1086 'Type' => 'image/png',
1087 'Encoding' => 'base64',
1088 'Data' => $self->invoice_barcode(0),
1089 'Filename' => 'barcode.png',
1090 'Content-ID' => "<$barcode_content_id>",
1092 $opt{'barcode_cid'} = $barcode_content_id;
1095 $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
1098 $alternative->attach(
1099 'Type' => 'text/html',
1100 'Encoding' => 'quoted-printable',
1101 'Data' => [ '<html>',
1104 ' '. encode_entities($return{'subject'}),
1107 ' <body bgcolor="#e8e8e8">',
1112 'Disposition' => 'inline',
1113 #'Filename' => 'invoice.pdf',
1117 my @otherparts = ();
1118 if ( $cust_main->email_csv_cdr ) {
1120 push @otherparts, build MIME::Entity
1121 'Type' => 'text/csv',
1122 'Encoding' => '7bit',
1123 'Data' => [ map { "$_\n" }
1124 $self->call_details('prepend_billed_number' => 1)
1126 'Disposition' => 'attachment',
1127 'Filename' => 'usage-'. $self->invnum. '.csv',
1132 if ( $conf->exists('invoice_email_pdf') ) {
1137 # multipart/alternative
1143 my $related = build MIME::Entity 'Type' => 'multipart/related',
1144 'Encoding' => '7bit';
1146 #false laziness w/Misc::send_email
1147 $related->head->replace('Content-type',
1148 $related->mime_type.
1149 '; boundary="'. $related->head->multipart_boundary. '"'.
1150 '; type=multipart/alternative'
1153 $related->add_part($alternative);
1155 $related->add_part($image) if $image;
1157 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1159 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1163 #no other attachment:
1165 # multipart/alternative
1170 $return{'content-type'} = 'multipart/related';
1171 if ($conf->exists('invoice-barcode') && $barcode) {
1172 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1174 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1176 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1177 #$return{'disposition'} = 'inline';
1183 if ( $conf->exists('invoice_email_pdf') ) {
1184 warn "$me creating PDF attachment"
1187 #mime parts arguments a la MIME::Entity->build().
1188 $return{'mimeparts'} = [
1189 { $self->mimebuild_pdf(\%opt) }
1193 if ( $conf->exists('invoice_email_pdf')
1194 and scalar($conf->config('invoice_email_pdf_note')) ) {
1196 warn "$me using 'invoice_email_pdf_note'"
1198 $return{'body'} = [ map { $_ . "\n" }
1199 $conf->config('invoice_email_pdf_note')
1204 warn "$me not using 'invoice_email_pdf_note'"
1206 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1207 $return{'body'} = $args{'print_text'};
1209 $return{'body'} = [ $self->print_text(\%opt) ];
1222 Returns a list suitable for passing to MIME::Entity->build(), representing
1223 this invoice as PDF attachment.
1230 'Type' => 'application/pdf',
1231 'Encoding' => 'base64',
1232 'Data' => [ $self->print_pdf(@_) ],
1233 'Disposition' => 'attachment',
1234 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1238 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1240 Sends this invoice to the destinations configured for this customer: sends
1241 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1243 Options can be passed as a hashref (recommended) or as a list of up to
1244 four values for templatename, agentnum, invoice_from and amount.
1246 I<template>, if specified, is the name of a suffix for alternate invoices.
1248 I<agentnum>, if specified, means that this invoice will only be sent for customers
1249 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1250 single agent) or an arrayref of agentnums.
1252 I<invoice_from>, if specified, overrides the default email invoice From: address.
1254 I<amount>, if specified, only sends the invoice if the total amount owed on this
1255 invoice and all older invoices is greater than the specified amount.
1257 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1261 sub queueable_send {
1264 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1265 or die "invalid invoice number: " . $opt{invnum};
1267 my @args = ( $opt{template}, $opt{agentnum} );
1268 push @args, $opt{invoice_from}
1269 if exists($opt{invoice_from}) && $opt{invoice_from};
1271 my $error = $self->send( @args );
1272 die $error if $error;
1278 my $conf = $self->conf;
1280 my( $template, $invoice_from, $notice_name );
1282 my $balance_over = 0;
1286 $template = $opt->{'template'} || '';
1287 if ( $agentnums = $opt->{'agentnum'} ) {
1288 $agentnums = [ $agentnums ] unless ref($agentnums);
1290 $invoice_from = $opt->{'invoice_from'};
1291 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1292 $notice_name = $opt->{'notice_name'};
1294 $template = scalar(@_) ? shift : '';
1295 if ( scalar(@_) && $_[0] ) {
1296 $agentnums = ref($_[0]) ? shift : [ shift ];
1298 $invoice_from = shift if scalar(@_);
1299 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1302 return 'N/A' unless ! $agentnums
1303 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1306 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1308 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1309 $conf->config('invoice_from', $self->cust_main->agentnum );
1312 'template' => $template,
1313 'invoice_from' => $invoice_from,
1314 'notice_name' => ( $notice_name || 'Invoice' ),
1317 my @invoicing_list = $self->cust_main->invoicing_list;
1319 #$self->email_invoice(\%opt)
1321 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1323 #$self->print_invoice(\%opt)
1325 if grep { $_ eq 'POST' } @invoicing_list; #postal
1327 $self->fax_invoice(\%opt)
1328 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1334 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1336 Emails this invoice.
1338 Options can be passed as a hashref (recommended) or as a list of up to
1339 two values for templatename and invoice_from.
1341 I<template>, if specified, is the name of a suffix for alternate invoices.
1343 I<invoice_from>, if specified, overrides the default email invoice From: address.
1345 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1349 sub queueable_email {
1352 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1353 or die "invalid invoice number: " . $opt{invnum};
1355 my %args = ( 'template' => $opt{template} );
1356 $args{$_} = $opt{$_}
1357 foreach grep { exists($opt{$_}) && $opt{$_} }
1358 qw( invoice_from notice_name no_coupon );
1360 my $error = $self->email( \%args );
1361 die $error if $error;
1365 #sub email_invoice {
1368 return if $self->hide;
1369 my $conf = $self->conf;
1371 my( $template, $invoice_from, $notice_name, $no_coupon );
1374 $template = $opt->{'template'} || '';
1375 $invoice_from = $opt->{'invoice_from'};
1376 $notice_name = $opt->{'notice_name'} || 'Invoice';
1377 $no_coupon = $opt->{'no_coupon'} || 0;
1379 $template = scalar(@_) ? shift : '';
1380 $invoice_from = shift if scalar(@_);
1381 $notice_name = 'Invoice';
1385 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1386 $conf->config('invoice_from', $self->cust_main->agentnum );
1388 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1389 $self->cust_main->invoicing_list;
1391 if ( ! @invoicing_list ) { #no recipients
1392 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1393 die 'No recipients for customer #'. $self->custnum;
1395 #default: better to notify this person than silence
1396 @invoicing_list = ($invoice_from);
1400 my $subject = $self->email_subject($template);
1402 my $error = send_email(
1403 $self->generate_email(
1404 'from' => $invoice_from,
1405 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1406 'subject' => $subject,
1407 'template' => $template,
1408 'notice_name' => $notice_name,
1409 'no_coupon' => $no_coupon,
1412 die "can't email invoice: $error\n" if $error;
1413 #die "$error\n" if $error;
1419 my $conf = $self->conf;
1421 #my $template = scalar(@_) ? shift : '';
1424 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1427 my $cust_main = $self->cust_main;
1428 my $name = $cust_main->name;
1429 my $name_short = $cust_main->name_short;
1430 my $invoice_number = $self->invnum;
1431 my $invoice_date = $self->_date_pretty;
1433 eval qq("$subject");
1436 =item lpr_data HASHREF | [ TEMPLATE ]
1438 Returns the postscript or plaintext for this invoice as an arrayref.
1440 Options can be passed as a hashref (recommended) or as a single optional value
1443 I<template>, if specified, is the name of a suffix for alternate invoices.
1445 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1451 my $conf = $self->conf;
1452 my( $template, $notice_name );
1455 $template = $opt->{'template'} || '';
1456 $notice_name = $opt->{'notice_name'} || 'Invoice';
1458 $template = scalar(@_) ? shift : '';
1459 $notice_name = 'Invoice';
1463 'template' => $template,
1464 'notice_name' => $notice_name,
1467 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1468 [ $self->$method( \%opt ) ];
1471 =item print HASHREF | [ TEMPLATE ]
1473 Prints this invoice.
1475 Options can be passed as a hashref (recommended) or as a single optional
1478 I<template>, if specified, is the name of a suffix for alternate invoices.
1480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1484 #sub print_invoice {
1487 return if $self->hide;
1488 my $conf = $self->conf;
1490 my( $template, $notice_name );
1493 $template = $opt->{'template'} || '';
1494 $notice_name = $opt->{'notice_name'} || 'Invoice';
1496 $template = scalar(@_) ? shift : '';
1497 $notice_name = 'Invoice';
1501 'template' => $template,
1502 'notice_name' => $notice_name,
1505 if($conf->exists('invoice_print_pdf')) {
1506 # Add the invoice to the current batch.
1507 $self->batch_invoice(\%opt);
1510 do_print $self->lpr_data(\%opt);
1514 =item fax_invoice HASHREF | [ TEMPLATE ]
1518 Options can be passed as a hashref (recommended) or as a single optional
1521 I<template>, if specified, is the name of a suffix for alternate invoices.
1523 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1529 return if $self->hide;
1530 my $conf = $self->conf;
1532 my( $template, $notice_name );
1535 $template = $opt->{'template'} || '';
1536 $notice_name = $opt->{'notice_name'} || 'Invoice';
1538 $template = scalar(@_) ? shift : '';
1539 $notice_name = 'Invoice';
1542 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1543 unless $conf->exists('invoice_latex');
1545 my $dialstring = $self->cust_main->getfield('fax');
1549 'template' => $template,
1550 'notice_name' => $notice_name,
1553 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1554 'dialstring' => $dialstring,
1556 die $error if $error;
1560 =item batch_invoice [ HASHREF ]
1562 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1563 isn't an open batch, one will be created.
1568 my ($self, $opt) = @_;
1569 my $bill_batch = $self->get_open_bill_batch;
1570 my $cust_bill_batch = FS::cust_bill_batch->new({
1571 batchnum => $bill_batch->batchnum,
1572 invnum => $self->invnum,
1574 return $cust_bill_batch->insert($opt);
1577 =item get_open_batch
1579 Returns the currently open batch as an FS::bill_batch object, creating a new
1580 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1585 sub get_open_bill_batch {
1587 my $conf = $self->conf;
1588 my $hashref = { status => 'O' };
1589 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1590 ? $self->cust_main->agentnum
1592 my $batch = qsearchs('bill_batch', $hashref);
1593 return $batch if $batch;
1594 $batch = FS::bill_batch->new($hashref);
1595 my $error = $batch->insert;
1596 die $error if $error;
1600 =item ftp_invoice [ TEMPLATENAME ]
1602 Sends this invoice data via FTP.
1604 TEMPLATENAME is unused?
1610 my $conf = $self->conf;
1611 my $template = scalar(@_) ? shift : '';
1614 'protocol' => 'ftp',
1615 'server' => $conf->config('cust_bill-ftpserver'),
1616 'username' => $conf->config('cust_bill-ftpusername'),
1617 'password' => $conf->config('cust_bill-ftppassword'),
1618 'dir' => $conf->config('cust_bill-ftpdir'),
1619 'format' => $conf->config('cust_bill-ftpformat'),
1623 =item spool_invoice [ TEMPLATENAME ]
1625 Spools this invoice data (see L<FS::spool_csv>)
1627 TEMPLATENAME is unused?
1633 my $conf = $self->conf;
1634 my $template = scalar(@_) ? shift : '';
1637 'format' => $conf->config('cust_bill-spoolformat'),
1638 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1642 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1644 Like B<send>, but only sends the invoice if it is the newest open invoice for
1649 sub send_if_newest {
1654 grep { $_->owed > 0 }
1655 qsearch('cust_bill', {
1656 'custnum' => $self->custnum,
1657 #'_date' => { op=>'>', value=>$self->_date },
1658 'invnum' => { op=>'>', value=>$self->invnum },
1665 =item send_csv OPTION => VALUE, ...
1667 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1671 protocol - currently only "ftp"
1677 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1678 and YYMMDDHHMMSS is a timestamp.
1680 See L</print_csv> for a description of the output format.
1685 my($self, %opt) = @_;
1689 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1690 mkdir $spooldir, 0700 unless -d $spooldir;
1692 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1693 my $file = "$spooldir/$tracctnum.csv";
1695 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1697 open(CSV, ">$file") or die "can't open $file: $!";
1705 if ( $opt{protocol} eq 'ftp' ) {
1706 eval "use Net::FTP;";
1708 $net = Net::FTP->new($opt{server}) or die @$;
1710 die "unknown protocol: $opt{protocol}";
1713 $net->login( $opt{username}, $opt{password} )
1714 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1716 $net->binary or die "can't set binary mode";
1718 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1720 $net->put($file) or die "can't put $file: $!";
1730 Spools CSV invoice data.
1736 =item format - 'default' or 'billco'
1738 =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>).
1740 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1742 =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.
1749 my($self, %opt) = @_;
1751 my $cust_main = $self->cust_main;
1753 if ( $opt{'dest'} ) {
1754 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1755 $cust_main->invoicing_list;
1756 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1757 || ! keys %invoicing_list;
1760 if ( $opt{'balanceover'} ) {
1762 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1765 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1766 mkdir $spooldir, 0700 unless -d $spooldir;
1768 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1772 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1773 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1776 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1778 open(CSV, ">>$file") or die "can't open $file: $!";
1779 flock(CSV, LOCK_EX);
1784 if ( lc($opt{'format'}) eq 'billco' ) {
1786 flock(CSV, LOCK_UN);
1791 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1794 open(CSV,">>$file") or die "can't open $file: $!";
1795 flock(CSV, LOCK_EX);
1801 flock(CSV, LOCK_UN);
1808 =item print_csv OPTION => VALUE, ...
1810 Returns CSV data for this invoice.
1814 format - 'default' or 'billco'
1816 Returns a list consisting of two scalars. The first is a single line of CSV
1817 header information for this invoice. The second is one or more lines of CSV
1818 detail information for this invoice.
1820 If I<format> is not specified or "default", the fields of the CSV file are as
1823 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1827 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1829 B<record_type> is C<cust_bill> for the initial header line only. The
1830 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1831 fields are filled in.
1833 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1834 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1837 =item invnum - invoice number
1839 =item custnum - customer number
1841 =item _date - invoice date
1843 =item charged - total invoice amount
1845 =item first - customer first name
1847 =item last - customer first name
1849 =item company - company name
1851 =item address1 - address line 1
1853 =item address2 - address line 1
1863 =item pkg - line item description
1865 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1867 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1869 =item sdate - start date for recurring fee
1871 =item edate - end date for recurring fee
1875 If I<format> is "billco", the fields of the header CSV file are as follows:
1877 +-------------------------------------------------------------------+
1878 | FORMAT HEADER FILE |
1879 |-------------------------------------------------------------------|
1880 | Field | Description | Name | Type | Width |
1881 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1882 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1883 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1884 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1885 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1886 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1887 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1888 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1889 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1890 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1891 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1892 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1893 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1894 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1895 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1896 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1897 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1898 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1899 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1900 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1901 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1902 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1903 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1904 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1905 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1906 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1907 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1908 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1909 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1910 +-------+-------------------------------+------------+------+-------+
1912 If I<format> is "billco", the fields of the detail CSV file are as follows:
1914 FORMAT FOR DETAIL FILE
1916 Field | Description | Name | Type | Width
1917 1 | N/A-Leave Empty | RC | CHAR | 2
1918 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1919 3 | Account Number | TRACCTNUM | CHAR | 15
1920 4 | Invoice Number | TRINVOICE | CHAR | 15
1921 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1922 6 | Transaction Detail | DETAILS | CHAR | 100
1923 7 | Amount | AMT | NUM* | 9
1924 8 | Line Format Control** | LNCTRL | CHAR | 2
1925 9 | Grouping Code | GROUP | CHAR | 2
1926 10 | User Defined | ACCT CODE | CHAR | 15
1931 my($self, %opt) = @_;
1933 eval "use Text::CSV_XS";
1936 my $cust_main = $self->cust_main;
1938 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1940 if ( lc($opt{'format'}) eq 'billco' ) {
1943 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1945 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1947 my( $previous_balance, @unused ) = $self->previous; #previous balance
1949 my $pmt_cr_applied = 0;
1950 $pmt_cr_applied += $_->{'amount'}
1951 foreach ( $self->_items_payments, $self->_items_credits ) ;
1953 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1956 '', # 1 | N/A-Leave Empty CHAR 2
1957 '', # 2 | N/A-Leave Empty CHAR 15
1958 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1959 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1960 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1961 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1962 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1963 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1964 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1965 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1966 '', # 10 | Ancillary Billing Information CHAR 30
1967 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1968 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1971 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1974 $duedate, # 14 | Bill Due Date CHAR 10
1976 $previous_balance, # 15 | Previous Balance NUM* 9
1977 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1978 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1979 $totaldue, # 18 | Total Amt Due NUM* 9
1980 $totaldue, # 19 | Total Amt Due NUM* 9
1981 '', # 20 | 30 Day Aging NUM* 9
1982 '', # 21 | 60 Day Aging NUM* 9
1983 '', # 22 | 90 Day Aging NUM* 9
1984 'N', # 23 | Y/N CHAR 1
1985 '', # 24 | Remittance automation CHAR 100
1986 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1987 $self->custnum, # 26 | Customer Reference Number CHAR 15
1988 '0', # 27 | Federal Tax*** NUM* 9
1989 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1990 '0', # 29 | Other Taxes & Fees*** NUM* 9
1999 time2str("%x", $self->_date),
2000 sprintf("%.2f", $self->charged),
2001 ( map { $cust_main->getfield($_) }
2002 qw( first last company address1 address2 city state zip country ) ),
2004 ) or die "can't create csv";
2007 my $header = $csv->string. "\n";
2010 if ( lc($opt{'format'}) eq 'billco' ) {
2013 foreach my $item ( $self->_items_pkg ) {
2016 '', # 1 | N/A-Leave Empty CHAR 2
2017 '', # 2 | N/A-Leave Empty CHAR 15
2018 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2019 $self->invnum, # 4 | Invoice Number CHAR 15
2020 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2021 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2022 $item->{'amount'}, # 7 | Amount NUM* 9
2023 '', # 8 | Line Format Control** CHAR 2
2024 '', # 9 | Grouping Code CHAR 2
2025 '', # 10 | User Defined CHAR 15
2028 $detail .= $csv->string. "\n";
2034 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2036 my($pkg, $setup, $recur, $sdate, $edate);
2037 if ( $cust_bill_pkg->pkgnum ) {
2039 ($pkg, $setup, $recur, $sdate, $edate) = (
2040 $cust_bill_pkg->part_pkg->pkg,
2041 ( $cust_bill_pkg->setup != 0
2042 ? sprintf("%.2f", $cust_bill_pkg->setup )
2044 ( $cust_bill_pkg->recur != 0
2045 ? sprintf("%.2f", $cust_bill_pkg->recur )
2047 ( $cust_bill_pkg->sdate
2048 ? time2str("%x", $cust_bill_pkg->sdate)
2050 ($cust_bill_pkg->edate
2051 ?time2str("%x", $cust_bill_pkg->edate)
2055 } else { #pkgnum tax
2056 next unless $cust_bill_pkg->setup != 0;
2057 $pkg = $cust_bill_pkg->desc;
2058 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2059 ( $sdate, $edate ) = ( '', '' );
2065 ( map { '' } (1..11) ),
2066 ($pkg, $setup, $recur, $sdate, $edate)
2067 ) or die "can't create csv";
2069 $detail .= $csv->string. "\n";
2075 ( $header, $detail );
2081 Pays this invoice with a compliemntary payment. If there is an error,
2082 returns the error, otherwise returns false.
2088 my $cust_pay = new FS::cust_pay ( {
2089 'invnum' => $self->invnum,
2090 'paid' => $self->owed,
2093 'payinfo' => $self->cust_main->payinfo,
2101 Attempts to pay this invoice with a credit card payment via a
2102 Business::OnlinePayment realtime gateway. See
2103 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2104 for supported processors.
2110 $self->realtime_bop( 'CC', @_ );
2115 Attempts to pay this invoice with an electronic check (ACH) payment via a
2116 Business::OnlinePayment realtime gateway. See
2117 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2118 for supported processors.
2124 $self->realtime_bop( 'ECHECK', @_ );
2129 Attempts to pay this invoice with phone bill (LEC) payment via a
2130 Business::OnlinePayment realtime gateway. See
2131 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2132 for supported processors.
2138 $self->realtime_bop( 'LEC', @_ );
2142 my( $self, $method ) = (shift,shift);
2143 my $conf = $self->conf;
2146 my $cust_main = $self->cust_main;
2147 my $balance = $cust_main->balance;
2148 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2149 $amount = sprintf("%.2f", $amount);
2150 return "not run (balance $balance)" unless $amount > 0;
2152 my $description = 'Internet Services';
2153 if ( $conf->exists('business-onlinepayment-description') ) {
2154 my $dtempl = $conf->config('business-onlinepayment-description');
2156 my $agent_obj = $cust_main->agent
2157 or die "can't retreive agent for $cust_main (agentnum ".
2158 $cust_main->agentnum. ")";
2159 my $agent = $agent_obj->agent;
2160 my $pkgs = join(', ',
2161 map { $_->part_pkg->pkg }
2162 grep { $_->pkgnum } $self->cust_bill_pkg
2164 $description = eval qq("$dtempl");
2167 $cust_main->realtime_bop($method, $amount,
2168 'description' => $description,
2169 'invnum' => $self->invnum,
2170 #this didn't do what we want, it just calls apply_payments_and_credits
2172 'apply_to_invoice' => 1,
2175 #this changes application behavior: auto payments
2176 #triggered against a specific invoice are now applied
2177 #to that invoice instead of oldest open.
2183 =item batch_card OPTION => VALUE...
2185 Adds a payment for this invoice to the pending credit card batch (see
2186 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2187 runs the payment using a realtime gateway.
2192 my ($self, %options) = @_;
2193 my $cust_main = $self->cust_main;
2195 $options{invnum} = $self->invnum;
2197 $cust_main->batch_card(%options);
2200 sub _agent_template {
2202 $self->cust_main->agent_template;
2205 sub _agent_invoice_from {
2207 $self->cust_main->agent_invoice_from;
2210 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2212 Returns an text invoice, as a list of lines.
2214 Options can be passed as a hashref (recommended) or as a list of time, template
2215 and then any key/value pairs for any other options.
2217 I<time>, if specified, is used to control the printing of overdue messages. The
2218 default is now. It isn't the date of the invoice; that's the `_date' field.
2219 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2220 L<Time::Local> and L<Date::Parse> for conversion functions.
2222 I<template>, if specified, is the name of a suffix for alternate invoices.
2224 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2230 my( $today, $template, %opt );
2232 %opt = %{ shift() };
2233 $today = delete($opt{'time'}) || '';
2234 $template = delete($opt{template}) || '';
2236 ( $today, $template, %opt ) = @_;
2239 my %params = ( 'format' => 'template' );
2240 $params{'time'} = $today if $today;
2241 $params{'template'} = $template if $template;
2242 $params{$_} = $opt{$_}
2243 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2245 $self->print_generic( %params );
2248 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2250 Internal method - returns a filename of a filled-in LaTeX template for this
2251 invoice (Note: add ".tex" to get the actual filename), and a filename of
2252 an associated logo (with the .eps extension included).
2254 See print_ps and print_pdf for methods that return PostScript and PDF output.
2256 Options can be passed as a hashref (recommended) or as a list of time, template
2257 and then any key/value pairs for any other options.
2259 I<time>, if specified, is used to control the printing of overdue messages. The
2260 default is now. It isn't the date of the invoice; that's the `_date' field.
2261 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2262 L<Time::Local> and L<Date::Parse> for conversion functions.
2264 I<template>, if specified, is the name of a suffix for alternate invoices.
2266 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2272 my $conf = $self->conf;
2273 my( $today, $template, %opt );
2275 %opt = %{ shift() };
2276 $today = delete($opt{'time'}) || '';
2277 $template = delete($opt{template}) || '';
2279 ( $today, $template, %opt ) = @_;
2282 my %params = ( 'format' => 'latex' );
2283 $params{'time'} = $today if $today;
2284 $params{'template'} = $template if $template;
2285 $params{$_} = $opt{$_}
2286 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2288 $template ||= $self->_agent_template;
2290 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2291 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2295 ) or die "can't open temp file: $!\n";
2297 my $agentnum = $self->cust_main->agentnum;
2299 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2300 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2301 or die "can't write temp file: $!\n";
2303 print $lh $conf->config_binary('logo.eps', $agentnum)
2304 or die "can't write temp file: $!\n";
2307 $params{'logo_file'} = $lh->filename;
2309 if($conf->exists('invoice-barcode')){
2310 my $png_file = $self->invoice_barcode($dir);
2311 my $eps_file = $png_file;
2312 $eps_file =~ s/\.png$/.eps/g;
2313 $png_file =~ /(barcode.*png)/;
2315 $eps_file =~ /(barcode.*eps)/;
2318 my $curr_dir = cwd();
2320 # after painfuly long experimentation, it was determined that sam2p won't
2321 # accept : and other chars in the path, no matter how hard I tried to
2322 # escape them, hence the chdir (and chdir back, just to be safe)
2323 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2324 or die "sam2p failed: $!\n";
2328 $params{'barcode_file'} = $eps_file;
2331 my @filled_in = $self->print_generic( %params );
2333 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2337 ) or die "can't open temp file: $!\n";
2338 binmode($fh, ':utf8'); # language support
2339 print $fh join('', @filled_in );
2342 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2343 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2347 =item invoice_barcode DIR_OR_FALSE
2349 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2350 it is taken as the temp directory where the PNG file will be generated and the
2351 PNG file name is returned. Otherwise, the PNG image itself is returned.
2355 sub invoice_barcode {
2356 my ($self, $dir) = (shift,shift);
2358 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2359 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2360 my $gd = $gdbar->plot(Height => 30);
2363 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2367 ) or die "can't open temp file: $!\n";
2368 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2369 my $png_file = $bh->filename;
2376 =item print_generic OPTION => VALUE ...
2378 Internal method - returns a filled-in template for this invoice as a scalar.
2380 See print_ps and print_pdf for methods that return PostScript and PDF output.
2382 Non optional options include
2383 format - latex, html, template
2385 Optional options include
2387 template - a value used as a suffix for a configuration template
2389 time - a value used to control the printing of overdue messages. The
2390 default is now. It isn't the date of the invoice; that's the `_date' field.
2391 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2392 L<Time::Local> and L<Date::Parse> for conversion functions.
2396 unsquelch_cdr - overrides any per customer cdr squelching when true
2398 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2400 locale - override customer's locale
2404 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2405 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2406 # yes: fixed width/plain text printing will be borked
2408 my( $self, %params ) = @_;
2409 my $conf = $self->conf;
2410 my $today = $params{today} ? $params{today} : time;
2411 warn "$me print_generic called on $self with suffix $params{template}\n"
2414 my $format = $params{format};
2415 die "Unknown format: $format"
2416 unless $format =~ /^(latex|html|template)$/;
2418 my $cust_main = $self->cust_main;
2419 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2420 unless $cust_main->payname
2421 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2423 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2424 'html' => [ '<%=', '%>' ],
2425 'template' => [ '{', '}' ],
2428 warn "$me print_generic creating template\n"
2431 #create the template
2432 my $template = $params{template} ? $params{template} : $self->_agent_template;
2433 my $templatefile = "invoice_$format";
2434 $templatefile .= "_$template"
2435 if length($template) && $conf->exists($templatefile."_$template");
2436 my @invoice_template = map "$_\n", $conf->config($templatefile)
2437 or die "cannot load config data $templatefile";
2440 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2441 #change this to a die when the old code is removed
2442 warn "old-style invoice template $templatefile; ".
2443 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2444 $old_latex = 'true';
2445 @invoice_template = _translate_old_latex_format(@invoice_template);
2448 warn "$me print_generic creating T:T object\n"
2451 my $text_template = new Text::Template(
2453 SOURCE => \@invoice_template,
2454 DELIMITERS => $delimiters{$format},
2457 warn "$me print_generic compiling T:T object\n"
2460 $text_template->compile()
2461 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2464 # additional substitution could possibly cause breakage in existing templates
2465 my %convert_maps = (
2467 'notes' => sub { map "$_", @_ },
2468 'footer' => sub { map "$_", @_ },
2469 'smallfooter' => sub { map "$_", @_ },
2470 'returnaddress' => sub { map "$_", @_ },
2471 'coupon' => sub { map "$_", @_ },
2472 'summary' => sub { map "$_", @_ },
2478 s/%%(.*)$/<!-- $1 -->/g;
2479 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2480 s/\\begin\{enumerate\}/<ol>/g;
2482 s/\\end\{enumerate\}/<\/ol>/g;
2483 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2492 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2494 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2499 s/\\\\\*?\s*$/<BR>/;
2500 s/\\hyphenation\{[\w\s\-]+}//;
2505 'coupon' => sub { "" },
2506 'summary' => sub { "" },
2513 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2514 s/\\begin\{enumerate\}//g;
2516 s/\\end\{enumerate\}//g;
2517 s/\\textbf\{(.*)\}/$1/g;
2524 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2526 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2531 s/\\\\\*?\s*$/\n/; # dubious
2532 s/\\hyphenation\{[\w\s\-]+}//;
2536 'coupon' => sub { "" },
2537 'summary' => sub { "" },
2542 # hashes for differing output formats
2543 my %nbsps = ( 'latex' => '~',
2544 'html' => '', # '&nbps;' would be nice
2545 'template' => '', # not used
2547 my $nbsp = $nbsps{$format};
2549 my %escape_functions = ( 'latex' => \&_latex_escape,
2550 'html' => \&_html_escape_nbsp,#\&encode_entities,
2551 'template' => sub { shift },
2553 my $escape_function = $escape_functions{$format};
2554 my $escape_function_nonbsp = ($format eq 'html')
2555 ? \&_html_escape : $escape_function;
2557 my %date_formats = ( 'latex' => $date_format_long,
2558 'html' => $date_format_long,
2561 $date_formats{'html'} =~ s/ / /g;
2563 my $date_format = $date_formats{$format};
2565 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2567 'html' => sub { return '<b>'. shift(). '</b>'
2569 'template' => sub { shift },
2571 my $embolden_function = $embolden_functions{$format};
2573 my %newline_tokens = ( 'latex' => '\\\\',
2577 my $newline_token = $newline_tokens{$format};
2579 warn "$me generating template variables\n"
2582 # generate template variables
2585 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2589 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2595 $returnaddress = join("\n",
2596 $conf->config_orbase("invoice_${format}returnaddress", $template)
2599 } elsif ( grep /\S/,
2600 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2602 my $convert_map = $convert_maps{$format}{'returnaddress'};
2605 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2610 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2612 my $convert_map = $convert_maps{$format}{'returnaddress'};
2613 $returnaddress = join( "\n", &$convert_map(
2614 map { s/( {2,})/'~' x length($1)/eg;
2618 ( $conf->config('company_name', $self->cust_main->agentnum),
2619 $conf->config('company_address', $self->cust_main->agentnum),
2626 my $warning = "Couldn't find a return address; ".
2627 "do you need to set the company_address configuration value?";
2629 $returnaddress = $nbsp;
2630 #$returnaddress = $warning;
2634 warn "$me generating invoice data\n"
2637 my $agentnum = $self->cust_main->agentnum;
2639 my %invoice_data = (
2642 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2643 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2644 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2645 'returnaddress' => $returnaddress,
2646 'agent' => &$escape_function($cust_main->agent->agent),
2649 'invnum' => $self->invnum,
2650 'date' => time2str($date_format, $self->_date),
2651 'today' => time2str($date_format_long, $today),
2652 'terms' => $self->terms,
2653 'template' => $template, #params{'template'},
2654 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2655 'current_charges' => sprintf("%.2f", $self->charged),
2656 'duedate' => $self->due_date2str($rdate_format), #date_format?
2659 'custnum' => $cust_main->display_custnum,
2660 'agent_custid' => &$escape_function($cust_main->agent_custid),
2661 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2662 payname company address1 address2 city state zip fax
2666 'ship_enable' => $conf->exists('invoice-ship_address'),
2667 'unitprices' => $conf->exists('invoice-unitprice'),
2668 'smallernotes' => $conf->exists('invoice-smallernotes'),
2669 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2670 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2672 #layout info -- would be fancy to calc some of this and bury the template
2674 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2675 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2676 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2677 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2678 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2679 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2680 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2681 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2682 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2683 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2685 # better hang on to conf_dir for a while (for old templates)
2686 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2688 #these are only used when doing paged plaintext
2695 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2696 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2697 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2698 # eval to avoid death for unimplemented languages
2699 my $dh = eval { Date::Language->new($info{'name'}) } ||
2700 Date::Language->new(); # fall back to English
2701 # prototype here to silence warnings
2702 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2703 # eventually use this date handle everywhere in here, too
2705 my $min_sdate = 999999999999;
2707 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2708 next unless $cust_bill_pkg->pkgnum > 0;
2709 $min_sdate = $cust_bill_pkg->sdate
2710 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2711 $max_edate = $cust_bill_pkg->edate
2712 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2715 $invoice_data{'bill_period'} = '';
2716 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2717 . " to " . time2str('%e %h', $max_edate)
2718 if ($max_edate != 0 && $min_sdate != 999999999999);
2720 $invoice_data{finance_section} = '';
2721 if ( $conf->config('finance_pkgclass') ) {
2723 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2724 $invoice_data{finance_section} = $pkg_class->categoryname;
2726 $invoice_data{finance_amount} = '0.00';
2727 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2729 my $countrydefault = $conf->config('countrydefault') || 'US';
2730 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2731 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2732 my $method = $prefix.$_;
2733 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2735 $invoice_data{'ship_country'} = ''
2736 if ( $invoice_data{'ship_country'} eq $countrydefault );
2738 $invoice_data{'cid'} = $params{'cid'}
2741 if ( $cust_main->country eq $countrydefault ) {
2742 $invoice_data{'country'} = '';
2744 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2748 $invoice_data{'address'} = \@address;
2750 $cust_main->payname.
2751 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2752 ? " (P.O. #". $cust_main->payinfo. ")"
2756 push @address, $cust_main->company
2757 if $cust_main->company;
2758 push @address, $cust_main->address1;
2759 push @address, $cust_main->address2
2760 if $cust_main->address2;
2762 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2763 push @address, $invoice_data{'country'}
2764 if $invoice_data{'country'};
2766 while (scalar(@address) < 5);
2768 $invoice_data{'logo_file'} = $params{'logo_file'}
2769 if $params{'logo_file'};
2770 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2771 if $params{'barcode_file'};
2772 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2773 if $params{'barcode_img'};
2774 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2775 if $params{'barcode_cid'};
2777 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2778 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2779 #my $balance_due = $self->owed + $pr_total - $cr_total;
2780 my $balance_due = $self->owed + $pr_total;
2782 # the customer's current balance as shown on the invoice before this one
2783 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2785 # the change in balance from that invoice to this one
2786 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2788 # the sum of amount owed on all previous invoices
2789 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2791 # the sum of amount owed on all invoices
2792 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2794 # info from customer's last invoice before this one, for some
2796 $invoice_data{'last_bill'} = {};
2797 my $last_bill = $pr_cust_bill[-1];
2799 $invoice_data{'last_bill'} = {
2800 '_date' => $last_bill->_date, #unformatted
2801 # all we need for now
2805 my $summarypage = '';
2806 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2809 $invoice_data{'summarypage'} = $summarypage;
2811 warn "$me substituting variables in notes, footer, smallfooter\n"
2814 my @include = (qw( notes footer smallfooter ));
2815 push @include, 'coupon' unless $params{'no_coupon'};
2816 foreach my $include (@include) {
2818 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2821 if ( $conf->exists($inc_file, $agentnum)
2822 && length( $conf->config($inc_file, $agentnum) ) ) {
2824 @inc_src = $conf->config($inc_file, $agentnum);
2828 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2830 my $convert_map = $convert_maps{$format}{$include};
2832 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2833 s/--\@\]/$delimiters{$format}[1]/g;
2836 &$convert_map( $conf->config($inc_file, $agentnum) );
2840 my $inc_tt = new Text::Template (
2842 SOURCE => [ map "$_\n", @inc_src ],
2843 DELIMITERS => $delimiters{$format},
2844 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2846 unless ( $inc_tt->compile() ) {
2847 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2848 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2852 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2854 $invoice_data{$include} =~ s/\n+$//
2855 if ($format eq 'latex');
2858 # let invoices use either of these as needed
2859 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2860 ? $cust_main->payinfo : '';
2861 $invoice_data{'po_line'} =
2862 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2863 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2866 my %money_chars = ( 'latex' => '',
2867 'html' => $conf->config('money_char') || '$',
2870 my $money_char = $money_chars{$format};
2872 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2873 'html' => $conf->config('money_char') || '$',
2876 my $other_money_char = $other_money_chars{$format};
2877 $invoice_data{'dollar'} = $other_money_char;
2879 my @detail_items = ();
2880 my @total_items = ();
2884 $invoice_data{'detail_items'} = \@detail_items;
2885 $invoice_data{'total_items'} = \@total_items;
2886 $invoice_data{'buf'} = \@buf;
2887 $invoice_data{'sections'} = \@sections;
2889 warn "$me generating sections\n"
2892 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2893 'subtotal' => $other_money_char.
2894 sprintf('%.2f', $pr_total),
2895 'summarized' => '', #why? $summarypage ? 'Y' : '',
2897 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2898 join(' / ', map { $cust_main->balance_date_range(@$_) }
2899 $self->_prior_month30s
2901 if $conf->exists('invoice_include_aging');
2904 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2905 'subtotal' => $taxtotal, # adjusted below
2907 my $tax_weight = _pkg_category($tax_section->{description})
2908 ? _pkg_category($tax_section->{description})->weight
2910 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2911 $tax_section->{'sort_weight'} = $tax_weight;
2914 my $adjusttotal = 0;
2915 my $adjust_section = { 'description' =>
2916 $self->mt('Credits, Payments, and Adjustments'),
2917 'subtotal' => 0, # adjusted below
2919 my $adjust_weight = _pkg_category($adjust_section->{description})
2920 ? _pkg_category($adjust_section->{description})->weight
2922 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2923 $adjust_section->{'sort_weight'} = $adjust_weight;
2925 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2926 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2927 $invoice_data{'multisection'} = $multisection;
2928 my $late_sections = [];
2929 my $extra_sections = [];
2930 my $extra_lines = ();
2931 if ( $multisection ) {
2932 ($extra_sections, $extra_lines) =
2933 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2934 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2936 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2938 push @detail_items, @$extra_lines if $extra_lines;
2940 $self->_items_sections( $late_sections, # this could stand a refactor
2942 $escape_function_nonbsp,
2946 if ($conf->exists('svc_phone_sections')) {
2947 my ($phone_sections, $phone_lines) =
2948 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2949 push @{$late_sections}, @$phone_sections;
2950 push @detail_items, @$phone_lines;
2952 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2953 my ($accountcode_section, $accountcode_lines) =
2954 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2955 if ( scalar(@$accountcode_lines) ) {
2956 push @{$late_sections}, $accountcode_section;
2957 push @detail_items, @$accountcode_lines;
2960 } else {# not multisection
2961 # make a default section
2962 push @sections, { 'description' => '', 'subtotal' => '',
2963 'no_subtotal' => 1 };
2964 # and calculate the finance charge total, since it won't get done otherwise.
2965 # XXX possibly other totals?
2966 # XXX possibly finance_pkgclass should not be used in this manner?
2967 if ( $conf->exists('finance_pkgclass') ) {
2968 my @finance_charges;
2969 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2970 if ( grep { $_->section eq $invoice_data{finance_section} }
2971 $cust_bill_pkg->cust_bill_pkg_display ) {
2972 # I think these are always setup fees, but just to be sure...
2973 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2976 $invoice_data{finance_amount} =
2977 sprintf('%.2f', sum( @finance_charges ) || 0);
2981 unless ( $conf->exists('disable_previous_balance')
2982 || $conf->exists('previous_balance-summary_only')
2986 warn "$me adding previous balances\n"
2989 foreach my $line_item ( $self->_items_previous ) {
2992 ext_description => [],
2994 $detail->{'ref'} = $line_item->{'pkgnum'};
2995 $detail->{'quantity'} = 1;
2996 $detail->{'section'} = $previous_section;
2997 $detail->{'description'} = &$escape_function($line_item->{'description'});
2998 if ( exists $line_item->{'ext_description'} ) {
2999 @{$detail->{'ext_description'}} = map {
3000 &$escape_function($_);
3001 } @{$line_item->{'ext_description'}};
3003 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
3004 $line_item->{'amount'};
3005 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3007 push @detail_items, $detail;
3008 push @buf, [ $detail->{'description'},
3009 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3015 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
3016 push @buf, ['','-----------'];
3017 push @buf, [ $self->mt('Total Previous Balance'),
3018 $money_char. sprintf("%10.2f", $pr_total) ];
3022 if ( $conf->exists('svc_phone-did-summary') ) {
3023 warn "$me adding DID summary\n"
3026 my ($didsummary,$minutes) = $self->_did_summary;
3027 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3029 { 'description' => $didsummary_desc,
3030 'ext_description' => [ $didsummary, $minutes ],
3034 foreach my $section (@sections, @$late_sections) {
3036 warn "$me adding section \n". Dumper($section)
3039 # begin some normalization
3040 $section->{'subtotal'} = $section->{'amount'}
3042 && !exists($section->{subtotal})
3043 && exists($section->{amount});
3045 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3046 if ( $invoice_data{finance_section} &&
3047 $section->{'description'} eq $invoice_data{finance_section} );
3049 $section->{'subtotal'} = $other_money_char.
3050 sprintf('%.2f', $section->{'subtotal'})
3053 # continue some normalization
3054 $section->{'amount'} = $section->{'subtotal'}
3058 if ( $section->{'description'} ) {
3059 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3064 warn "$me setting options\n"
3067 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3069 $options{'section'} = $section if $multisection;
3070 $options{'format'} = $format;
3071 $options{'escape_function'} = $escape_function;
3072 $options{'no_usage'} = 1 unless $unsquelched;
3073 $options{'unsquelched'} = $unsquelched;
3074 $options{'summary_page'} = $summarypage;
3075 $options{'skip_usage'} =
3076 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3077 $options{'multilocation'} = $multilocation;
3078 $options{'multisection'} = $multisection;
3080 warn "$me searching for line items\n"
3083 foreach my $line_item ( $self->_items_pkg(%options) ) {
3085 warn "$me adding line item $line_item\n"
3089 ext_description => [],
3091 $detail->{'ref'} = $line_item->{'pkgnum'};
3092 $detail->{'quantity'} = $line_item->{'quantity'};
3093 $detail->{'section'} = $section;
3094 $detail->{'description'} = &$escape_function($line_item->{'description'});
3095 if ( exists $line_item->{'ext_description'} ) {
3096 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3098 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3099 $line_item->{'amount'};
3100 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3101 $line_item->{'unit_amount'};
3102 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3104 $detail->{'sdate'} = $line_item->{'sdate'};
3105 $detail->{'edate'} = $line_item->{'edate'};
3106 $detail->{'seconds'} = $line_item->{'seconds'};
3108 push @detail_items, $detail;
3109 push @buf, ( [ $detail->{'description'},
3110 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3112 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3116 if ( $section->{'description'} ) {
3117 push @buf, ( ['','-----------'],
3118 [ $section->{'description'}. ' sub-total',
3119 $section->{'subtotal'} # already formatted this
3128 $invoice_data{current_less_finance} =
3129 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3131 if ( $multisection && !$conf->exists('disable_previous_balance')
3132 || $conf->exists('previous_balance-summary_only') )
3134 unshift @sections, $previous_section if $pr_total;
3137 warn "$me adding taxes\n"
3140 foreach my $tax ( $self->_items_tax ) {
3142 $taxtotal += $tax->{'amount'};
3144 my $description = &$escape_function( $tax->{'description'} );
3145 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3147 if ( $multisection ) {
3149 my $money = $old_latex ? '' : $money_char;
3150 push @detail_items, {
3151 ext_description => [],
3154 description => $description,
3155 amount => $money. $amount,
3157 section => $tax_section,
3162 push @total_items, {
3163 'total_item' => $description,
3164 'total_amount' => $other_money_char. $amount,
3169 push @buf,[ $description,
3170 $money_char. $amount,
3177 $total->{'total_item'} = $self->mt('Sub-total');
3178 $total->{'total_amount'} =
3179 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3181 if ( $multisection ) {
3182 $tax_section->{'subtotal'} = $other_money_char.
3183 sprintf('%.2f', $taxtotal);
3184 $tax_section->{'pretotal'} = 'New charges sub-total '.
3185 $total->{'total_amount'};
3186 push @sections, $tax_section if $taxtotal;
3188 unshift @total_items, $total;
3191 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3193 push @buf,['','-----------'];
3194 push @buf,[$self->mt(
3195 $conf->exists('disable_previous_balance')
3197 : 'Total New Charges'
3199 $money_char. sprintf("%10.2f",$self->charged) ];
3205 $item = $conf->config('previous_balance-exclude_from_total')
3206 || 'Total New Charges'
3207 if $conf->exists('previous_balance-exclude_from_total');
3208 my $amount = $self->charged +
3209 ( $conf->exists('disable_previous_balance') ||
3210 $conf->exists('previous_balance-exclude_from_total')
3214 $total->{'total_item'} = &$embolden_function($self->mt($item));
3215 $total->{'total_amount'} =
3216 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3217 if ( $multisection ) {
3218 if ( $adjust_section->{'sort_weight'} ) {
3219 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3220 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3222 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3223 $other_money_char. sprintf('%.2f', $self->charged );
3226 push @total_items, $total;
3228 push @buf,['','-----------'];
3231 sprintf( '%10.2f', $amount )
3236 unless ( $conf->exists('disable_previous_balance') ) {
3237 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3240 my $credittotal = 0;
3241 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3244 $total->{'total_item'} = &$escape_function($credit->{'description'});
3245 $credittotal += $credit->{'amount'};
3246 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3247 $adjusttotal += $credit->{'amount'};
3248 if ( $multisection ) {
3249 my $money = $old_latex ? '' : $money_char;
3250 push @detail_items, {
3251 ext_description => [],
3254 description => &$escape_function($credit->{'description'}),
3255 amount => $money. $credit->{'amount'},
3257 section => $adjust_section,
3260 push @total_items, $total;
3264 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3267 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3268 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3272 my $paymenttotal = 0;
3273 foreach my $payment ( $self->_items_payments ) {
3275 $total->{'total_item'} = &$escape_function($payment->{'description'});
3276 $paymenttotal += $payment->{'amount'};
3277 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3278 $adjusttotal += $payment->{'amount'};
3279 if ( $multisection ) {
3280 my $money = $old_latex ? '' : $money_char;
3281 push @detail_items, {
3282 ext_description => [],
3285 description => &$escape_function($payment->{'description'}),
3286 amount => $money. $payment->{'amount'},
3288 section => $adjust_section,
3291 push @total_items, $total;
3293 push @buf, [ $payment->{'description'},
3294 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3297 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3299 if ( $multisection ) {
3300 $adjust_section->{'subtotal'} = $other_money_char.
3301 sprintf('%.2f', $adjusttotal);
3302 push @sections, $adjust_section
3303 unless $adjust_section->{sort_weight};
3306 # create Balance Due message
3309 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3310 $total->{'total_amount'} =
3311 &$embolden_function(
3312 $other_money_char. sprintf('%.2f', $summarypage
3314 $self->billing_balance
3315 : $self->owed + $pr_total
3318 if ( $multisection && !$adjust_section->{sort_weight} ) {
3319 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3320 $total->{'total_amount'};
3322 push @total_items, $total;
3324 push @buf,['','-----------'];
3325 push @buf,[$self->balance_due_msg, $money_char.
3326 sprintf("%10.2f", $balance_due ) ];
3329 if ( $conf->exists('previous_balance-show_credit')
3330 and $cust_main->balance < 0 ) {
3331 my $credit_total = {
3332 'total_item' => &$embolden_function($self->credit_balance_msg),
3333 'total_amount' => &$embolden_function(
3334 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3337 if ( $multisection ) {
3338 $adjust_section->{'posttotal'} .= $newline_token .
3339 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3342 push @total_items, $credit_total;
3344 push @buf,['','-----------'];
3345 push @buf,[$self->credit_balance_msg, $money_char.
3346 sprintf("%10.2f", -$cust_main->balance ) ];
3350 if ( $multisection ) {
3351 if ($conf->exists('svc_phone_sections')) {
3353 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3354 $total->{'total_amount'} =
3355 &$embolden_function(
3356 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3358 my $last_section = pop @sections;
3359 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3360 $total->{'total_amount'};
3361 push @sections, $last_section;
3363 push @sections, @$late_sections
3367 # make a discounts-available section, even without multisection
3368 if ( $conf->exists('discount-show_available')
3369 and my @discounts_avail = $self->_items_discounts_avail ) {
3370 my $discount_section = {
3371 'description' => $self->mt('Discounts Available'),
3376 push @sections, $discount_section;
3377 push @detail_items, map { +{
3378 'ref' => '', #should this be something else?
3379 'section' => $discount_section,
3380 'description' => &$escape_function( $_->{description} ),
3381 'amount' => $money_char . &$escape_function( $_->{amount} ),
3382 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3383 } } @discounts_avail;
3386 # All sections and items are built; now fill in templates.
3387 my @includelist = ();
3388 push @includelist, 'summary' if $summarypage;
3389 foreach my $include ( @includelist ) {
3391 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3394 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3396 @inc_src = $conf->config($inc_file, $agentnum);
3400 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3402 my $convert_map = $convert_maps{$format}{$include};
3404 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3405 s/--\@\]/$delimiters{$format}[1]/g;
3408 &$convert_map( $conf->config($inc_file, $agentnum) );
3412 my $inc_tt = new Text::Template (
3414 SOURCE => [ map "$_\n", @inc_src ],
3415 DELIMITERS => $delimiters{$format},
3416 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3418 unless ( $inc_tt->compile() ) {
3419 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3420 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3424 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3426 $invoice_data{$include} =~ s/\n+$//
3427 if ($format eq 'latex');
3432 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3433 /invoice_lines\((\d*)\)/;
3434 $invoice_lines += $1 || scalar(@buf);
3437 die "no invoice_lines() functions in template?"
3438 if ( $format eq 'template' && !$wasfunc );
3440 if ($format eq 'template') {
3442 if ( $invoice_lines ) {
3443 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3444 $invoice_data{'total_pages'}++
3445 if scalar(@buf) % $invoice_lines;
3448 #setup subroutine for the template
3449 $invoice_data{invoice_lines} = sub {
3450 my $lines = shift || scalar(@buf);
3462 push @collect, split("\n",
3463 $text_template->fill_in( HASH => \%invoice_data )
3465 $invoice_data{'page'}++;
3467 map "$_\n", @collect;
3469 # this is where we actually create the invoice
3470 warn "filling in template for invoice ". $self->invnum. "\n"
3472 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3475 $text_template->fill_in(HASH => \%invoice_data);
3479 # helper routine for generating date ranges
3480 sub _prior_month30s {
3483 [ 1, 2592000 ], # 0-30 days ago
3484 [ 2592000, 5184000 ], # 30-60 days ago
3485 [ 5184000, 7776000 ], # 60-90 days ago
3486 [ 7776000, 0 ], # 90+ days ago
3489 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3490 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3495 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3497 Returns an postscript invoice, as a scalar.
3499 Options can be passed as a hashref (recommended) or as a list of time, template
3500 and then any key/value pairs for any other options.
3502 I<time> an optional value used to control the printing of overdue messages. The
3503 default is now. It isn't the date of the invoice; that's the `_date' field.
3504 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3505 L<Time::Local> and L<Date::Parse> for conversion functions.
3507 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3514 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3515 my $ps = generate_ps($file);
3517 unlink($barcodefile) if $barcodefile;
3522 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3524 Returns an PDF invoice, as a scalar.
3526 Options can be passed as a hashref (recommended) or as a list of time, template
3527 and then any key/value pairs for any other options.
3529 I<time> an optional value used to control the printing of overdue messages. The
3530 default is now. It isn't the date of the invoice; that's the `_date' field.
3531 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3532 L<Time::Local> and L<Date::Parse> for conversion functions.
3534 I<template>, if specified, is the name of a suffix for alternate invoices.
3536 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3543 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3544 my $pdf = generate_pdf($file);
3546 unlink($barcodefile) if $barcodefile;
3551 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3553 Returns an HTML invoice, as a scalar.
3555 I<time> an optional value used to control the printing of overdue messages. The
3556 default is now. It isn't the date of the invoice; that's the `_date' field.
3557 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3558 L<Time::Local> and L<Date::Parse> for conversion functions.
3560 I<template>, if specified, is the name of a suffix for alternate invoices.
3562 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3564 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3565 when emailing the invoice as part of a multipart/related MIME email.
3573 %params = %{ shift() };
3575 $params{'time'} = shift;
3576 $params{'template'} = shift;
3577 $params{'cid'} = shift;
3580 $params{'format'} = 'html';
3582 $self->print_generic( %params );
3585 # quick subroutine for print_latex
3587 # There are ten characters that LaTeX treats as special characters, which
3588 # means that they do not simply typeset themselves:
3589 # # $ % & ~ _ ^ \ { }
3591 # TeX ignores blanks following an escaped character; if you want a blank (as
3592 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3596 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3597 $value =~ s/([<>])/\$$1\$/g;
3603 encode_entities($value);
3607 sub _html_escape_nbsp {
3608 my $value = _html_escape(shift);
3609 $value =~ s/ +/ /g;
3613 #utility methods for print_*
3615 sub _translate_old_latex_format {
3616 warn "_translate_old_latex_format called\n"
3623 if ( $line =~ /^%%Detail\s*$/ ) {
3625 push @template, q![@--!,
3626 q! foreach my $_tr_line (@detail_items) {!,
3627 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3628 q! $_tr_line->{'description'} .= !,
3629 q! "\\tabularnewline\n~~".!,
3630 q! join( "\\tabularnewline\n~~",!,
3631 q! @{$_tr_line->{'ext_description'}}!,
3635 while ( ( my $line_item_line = shift )
3636 !~ /^%%EndDetail\s*$/ ) {
3637 $line_item_line =~ s/'/\\'/g; # nice LTS
3638 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3639 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3640 push @template, " \$OUT .= '$line_item_line';";
3643 push @template, '}',
3646 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3648 push @template, '[@--',
3649 ' foreach my $_tr_line (@total_items) {';
3651 while ( ( my $total_item_line = shift )
3652 !~ /^%%EndTotalDetails\s*$/ ) {
3653 $total_item_line =~ s/'/\\'/g; # nice LTS
3654 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3655 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3656 push @template, " \$OUT .= '$total_item_line';";
3659 push @template, '}',
3663 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3664 push @template, $line;
3670 warn "$_\n" foreach @template;
3678 my $conf = $self->conf;
3680 #check for an invoice-specific override
3681 return $self->invoice_terms if $self->invoice_terms;
3683 #check for a customer- specific override
3684 my $cust_main = $self->cust_main;
3685 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3687 #use configured default
3688 $conf->config('invoice_default_terms') || '';
3694 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3695 $duedate = $self->_date() + ( $1 * 86400 );
3702 $self->due_date ? time2str(shift, $self->due_date) : '';
3705 sub balance_due_msg {
3707 my $msg = $self->mt('Balance Due');
3708 return $msg unless $self->terms;
3709 if ( $self->due_date ) {
3710 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3711 $self->due_date2str($date_format);
3712 } elsif ( $self->terms ) {
3713 $msg .= ' - '. $self->terms;
3718 sub balance_due_date {
3720 my $conf = $self->conf;
3722 if ( $conf->exists('invoice_default_terms')
3723 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3724 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3729 sub credit_balance_msg {
3731 $self->mt('Credit Balance Remaining')
3734 =item invnum_date_pretty
3736 Returns a string with the invoice number and date, for example:
3737 "Invoice #54 (3/20/2008)"
3741 sub invnum_date_pretty {
3743 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3748 Returns a string with the date, for example: "3/20/2008"
3754 time2str($date_format, $self->_date);
3757 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3759 Generate section information for all items appearing on this invoice.
3760 This will only be called for multi-section invoices.
3762 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3763 related display records (L<FS::cust_bill_pkg_display>) and organize
3764 them into two groups ("early" and "late" according to whether they come
3765 before or after the total), then into sections. A subtotal is calculated
3768 Section descriptions are returned in sort weight order. Each consists
3769 of a hash containing:
3771 description: the package category name, escaped
3772 subtotal: the total charges in that section
3773 tax_section: a flag indicating that the section contains only tax charges
3774 summarized: same as tax_section, for some reason
3775 sort_weight: the package category's sort weight
3777 If 'condense' is set on the display record, it also contains everything
3778 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3779 coderefs to generate parts of the invoice. This is not advised.
3783 LATE: an arrayref to push the "late" section hashes onto. The "early"
3784 group is simply returned from the method.
3786 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3787 Turning this on has the following effects:
3788 - Ignores display items with the 'summary' flag.
3789 - Combines all items into the "early" group.
3790 - Creates sections for all non-disabled package categories, even if they
3791 have no charges on this invoice, as well as a section with no name.
3793 ESCAPE: an escape function to use for section titles.
3795 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3796 sorted list. If there are any of these, section subtotals exclude
3799 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3800 passed through to C<_condense_section()>.
3804 use vars qw(%pkg_category_cache);
3805 sub _items_sections {
3808 my $summarypage = shift;
3810 my $extra_sections = shift;
3814 my %late_subtotal = ();
3817 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3820 my $usage = $cust_bill_pkg->usage;
3822 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3823 next if ( $display->summary && $summarypage );
3825 my $section = $display->section;
3826 my $type = $display->type;
3828 $not_tax{$section} = 1
3829 unless $cust_bill_pkg->pkgnum == 0;
3831 if ( $display->post_total && !$summarypage ) {
3832 if (! $type || $type eq 'S') {
3833 $late_subtotal{$section} += $cust_bill_pkg->setup
3834 if $cust_bill_pkg->setup != 0;
3838 $late_subtotal{$section} += $cust_bill_pkg->recur
3839 if $cust_bill_pkg->recur != 0;
3842 if ($type && $type eq 'R') {
3843 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3844 if $cust_bill_pkg->recur != 0;
3847 if ($type && $type eq 'U') {
3848 $late_subtotal{$section} += $usage
3849 unless scalar(@$extra_sections);
3854 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3856 if (! $type || $type eq 'S') {
3857 $subtotal{$section} += $cust_bill_pkg->setup
3858 if $cust_bill_pkg->setup != 0;
3862 $subtotal{$section} += $cust_bill_pkg->recur
3863 if $cust_bill_pkg->recur != 0;
3866 if ($type && $type eq 'R') {
3867 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3868 if $cust_bill_pkg->recur != 0;
3871 if ($type && $type eq 'U') {
3872 $subtotal{$section} += $usage
3873 unless scalar(@$extra_sections);
3882 %pkg_category_cache = ();
3884 push @$late, map { { 'description' => &{$escape}($_),
3885 'subtotal' => $late_subtotal{$_},
3887 'sort_weight' => ( _pkg_category($_)
3888 ? _pkg_category($_)->weight
3891 ((_pkg_category($_) && _pkg_category($_)->condense)
3892 ? $self->_condense_section($format)
3896 sort _sectionsort keys %late_subtotal;
3899 if ( $summarypage ) {
3900 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3901 map { $_->categoryname } qsearch('pkg_category', {});
3902 push @sections, '' if exists($subtotal{''});
3904 @sections = keys %subtotal;
3907 my @early = map { { 'description' => &{$escape}($_),
3908 'subtotal' => $subtotal{$_},
3909 'summarized' => $not_tax{$_} ? '' : 'Y',
3910 'tax_section' => $not_tax{$_} ? '' : 'Y',
3911 'sort_weight' => ( _pkg_category($_)
3912 ? _pkg_category($_)->weight
3915 ((_pkg_category($_) && _pkg_category($_)->condense)
3916 ? $self->_condense_section($format)
3921 push @early, @$extra_sections if $extra_sections;
3923 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3927 #helper subs for above
3930 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3934 my $categoryname = shift;
3935 $pkg_category_cache{$categoryname} ||=
3936 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3939 my %condensed_format = (
3940 'label' => [ qw( Description Qty Amount ) ],
3942 sub { shift->{description} },
3943 sub { shift->{quantity} },
3944 sub { my($href, %opt) = @_;
3945 ($opt{dollar} || ''). $href->{amount};
3948 'align' => [ qw( l r r ) ],
3949 'span' => [ qw( 5 1 1 ) ], # unitprices?
3950 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3953 sub _condense_section {
3954 my ( $self, $format ) = ( shift, shift );
3956 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3957 qw( description_generator
3960 total_line_generator
3965 sub _condensed_generator_defaults {
3966 my ( $self, $format ) = ( shift, shift );
3967 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3976 sub _condensed_header_generator {
3977 my ( $self, $format ) = ( shift, shift );
3979 my ( $f, $prefix, $suffix, $separator, $column ) =
3980 _condensed_generator_defaults($format);
3982 if ($format eq 'latex') {
3983 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3984 $suffix = "\\\\\n\\hline";
3987 sub { my ($d,$a,$s,$w) = @_;
3988 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3990 } elsif ( $format eq 'html' ) {
3991 $prefix = '<th></th>';
3995 sub { my ($d,$a,$s,$w) = @_;
3996 return qq!<th align="$html_align{$a}">$d</th>!;
4004 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4006 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
4009 $prefix. join($separator, @result). $suffix;
4014 sub _condensed_description_generator {
4015 my ( $self, $format ) = ( shift, shift );
4017 my ( $f, $prefix, $suffix, $separator, $column ) =
4018 _condensed_generator_defaults($format);
4020 my $money_char = '$';
4021 if ($format eq 'latex') {
4022 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4024 $separator = " & \n";
4026 sub { my ($d,$a,$s,$w) = @_;
4027 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4029 $money_char = '\\dollar';
4030 }elsif ( $format eq 'html' ) {
4031 $prefix = '"><td align="center"></td>';
4035 sub { my ($d,$a,$s,$w) = @_;
4036 return qq!<td align="$html_align{$a}">$d</td>!;
4038 #$money_char = $conf->config('money_char') || '$';
4039 $money_char = ''; # this is madness
4047 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4049 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4051 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4052 map { $f->{$_}->[$i] } qw(align span width)
4056 $prefix. join( $separator, @result ). $suffix;
4061 sub _condensed_total_generator {
4062 my ( $self, $format ) = ( shift, shift );
4064 my ( $f, $prefix, $suffix, $separator, $column ) =
4065 _condensed_generator_defaults($format);
4068 if ($format eq 'latex') {
4071 $separator = " & \n";
4073 sub { my ($d,$a,$s,$w) = @_;
4074 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4076 }elsif ( $format eq 'html' ) {
4080 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4082 sub { my ($d,$a,$s,$w) = @_;
4083 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4092 # my $r = &{$f->{fields}->[$i]}(@args);
4093 # $r .= ' Total' unless $i;
4095 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4097 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4098 map { $f->{$_}->[$i] } qw(align span width)
4102 $prefix. join( $separator, @result ). $suffix;
4107 =item total_line_generator FORMAT
4109 Returns a coderef used for generation of invoice total line items for this
4110 usage_class. FORMAT is either html or latex
4114 # should not be used: will have issues with hash element names (description vs
4115 # total_item and amount vs total_amount -- another array of functions?
4117 sub _condensed_total_line_generator {
4118 my ( $self, $format ) = ( shift, shift );
4120 my ( $f, $prefix, $suffix, $separator, $column ) =
4121 _condensed_generator_defaults($format);
4124 if ($format eq 'latex') {
4127 $separator = " & \n";
4129 sub { my ($d,$a,$s,$w) = @_;
4130 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4132 }elsif ( $format eq 'html' ) {
4136 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4138 sub { my ($d,$a,$s,$w) = @_;
4139 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4148 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4150 &{$column}( &{$f->{fields}->[$i]}(@args),
4151 map { $f->{$_}->[$i] } qw(align span width)
4155 $prefix. join( $separator, @result ). $suffix;
4160 #sub _items_extra_usage_sections {
4162 # my $escape = shift;
4164 # my %sections = ();
4166 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4167 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4169 # next unless $cust_bill_pkg->pkgnum > 0;
4171 # foreach my $section ( keys %usage_class ) {
4173 # my $usage = $cust_bill_pkg->usage($section);
4175 # next unless $usage && $usage > 0;
4177 # $sections{$section} ||= 0;
4178 # $sections{$section} += $usage;
4184 # map { { 'description' => &{$escape}($_),
4185 # 'subtotal' => $sections{$_},
4186 # 'summarized' => '',
4187 # 'tax_section' => '',
4190 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4194 sub _items_extra_usage_sections {
4196 my $conf = $self->conf;
4204 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4206 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4207 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4208 next unless $cust_bill_pkg->pkgnum > 0;
4210 foreach my $classnum ( keys %usage_class ) {
4211 my $section = $usage_class{$classnum}->classname;
4212 $classnums{$section} = $classnum;
4214 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4215 my $amount = $detail->amount;
4216 next unless $amount && $amount > 0;
4218 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4219 $sections{$section}{amount} += $amount; #subtotal
4220 $sections{$section}{calls}++;
4221 $sections{$section}{duration} += $detail->duration;
4223 my $desc = $detail->regionname;
4224 my $description = $desc;
4225 $description = substr($desc, 0, $maxlength). '...'
4226 if $format eq 'latex' && length($desc) > $maxlength;
4228 $lines{$section}{$desc} ||= {
4229 description => &{$escape}($description),
4230 #pkgpart => $part_pkg->pkgpart,
4231 pkgnum => $cust_bill_pkg->pkgnum,
4236 #unit_amount => $cust_bill_pkg->unitrecur,
4237 quantity => $cust_bill_pkg->quantity,
4238 product_code => 'N/A',
4239 ext_description => [],
4242 $lines{$section}{$desc}{amount} += $amount;
4243 $lines{$section}{$desc}{calls}++;
4244 $lines{$section}{$desc}{duration} += $detail->duration;
4250 my %sectionmap = ();
4251 foreach (keys %sections) {
4252 my $usage_class = $usage_class{$classnums{$_}};
4253 $sectionmap{$_} = { 'description' => &{$escape}($_),
4254 'amount' => $sections{$_}{amount}, #subtotal
4255 'calls' => $sections{$_}{calls},
4256 'duration' => $sections{$_}{duration},
4258 'tax_section' => '',
4259 'sort_weight' => $usage_class->weight,
4260 ( $usage_class->format
4261 ? ( map { $_ => $usage_class->$_($format) }
4262 qw( description_generator header_generator total_generator total_line_generator )
4269 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4273 foreach my $section ( keys %lines ) {
4274 foreach my $line ( keys %{$lines{$section}} ) {
4275 my $l = $lines{$section}{$line};
4276 $l->{section} = $sectionmap{$section};
4277 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4278 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4283 return(\@sections, \@lines);
4289 my $end = $self->_date;
4291 # start at date of previous invoice + 1 second or 0 if no previous invoice
4292 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4293 $start = 0 if !$start;
4296 my $cust_main = $self->cust_main;
4297 my @pkgs = $cust_main->all_pkgs;
4298 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4301 foreach my $pkg ( @pkgs ) {
4302 my @h_cust_svc = $pkg->h_cust_svc($end);
4303 foreach my $h_cust_svc ( @h_cust_svc ) {
4304 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4305 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4307 my $inserted = $h_cust_svc->date_inserted;
4308 my $deleted = $h_cust_svc->date_deleted;
4309 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4311 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4313 # DID either activated or ported in; cannot be both for same DID simultaneously
4314 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4315 && (!$phone_inserted->lnp_status
4316 || $phone_inserted->lnp_status eq ''
4317 || $phone_inserted->lnp_status eq 'native')) {
4320 else { # this one not so clean, should probably move to (h_)svc_phone
4321 my $phone_portedin = qsearchs( 'h_svc_phone',
4322 { 'svcnum' => $h_cust_svc->svcnum,
4323 'lnp_status' => 'portedin' },
4324 FS::h_svc_phone->sql_h_searchs($end),
4326 $num_portedin++ if $phone_portedin;
4329 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4330 if($deleted >= $start && $deleted <= $end && $phone_deleted
4331 && (!$phone_deleted->lnp_status
4332 || $phone_deleted->lnp_status ne 'portingout')) {
4335 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4336 && $phone_deleted->lnp_status
4337 && $phone_deleted->lnp_status eq 'portingout') {
4341 # increment usage minutes
4342 if ( $phone_inserted ) {
4343 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4344 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4347 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4350 # don't look at this service again
4351 push @seen, $h_cust_svc->svcnum;
4355 $minutes = sprintf("%d", $minutes);
4356 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4357 . "$num_deactivated Ported-Out: $num_portedout ",
4358 "Total Minutes: $minutes");
4361 sub _items_accountcode_cdr {
4366 my $section = { 'amount' => 0,
4369 'sort_weight' => '',
4371 'description' => 'Usage by Account Code',
4377 my %accountcodes = ();
4379 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4380 next unless $cust_bill_pkg->pkgnum > 0;
4382 my @header = $cust_bill_pkg->details_header;
4383 next unless scalar(@header);
4384 $section->{'header'} = join(',',@header);
4386 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4388 $section->{'header'} = $detail->formatted('format' => $format)
4389 if($detail->detail eq $section->{'header'});
4391 my $accountcode = $detail->accountcode;
4392 next unless $accountcode;
4394 my $amount = $detail->amount;
4395 next unless $amount && $amount > 0;
4397 $accountcodes{$accountcode} ||= {
4398 description => $accountcode,
4405 product_code => 'N/A',
4406 section => $section,
4407 ext_description => [ $section->{'header'} ],
4411 $section->{'amount'} += $amount;
4412 $accountcodes{$accountcode}{'amount'} += $amount;
4413 $accountcodes{$accountcode}{calls}++;
4414 $accountcodes{$accountcode}{duration} += $detail->duration;
4415 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4419 foreach my $l ( values %accountcodes ) {
4420 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4421 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4422 foreach my $sorted_detail ( @sorted_detail ) {
4423 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4425 delete $l->{detail_temp};
4429 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4431 return ($section,\@sorted_lines);
4434 sub _items_svc_phone_sections {
4436 my $conf = $self->conf;
4444 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4446 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4447 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4449 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4450 next unless $cust_bill_pkg->pkgnum > 0;
4452 my @header = $cust_bill_pkg->details_header;
4453 next unless scalar(@header);
4455 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4457 my $phonenum = $detail->phonenum;
4458 next unless $phonenum;
4460 my $amount = $detail->amount;
4461 next unless $amount && $amount > 0;
4463 $sections{$phonenum} ||= { 'amount' => 0,
4466 'sort_weight' => -1,
4467 'phonenum' => $phonenum,
4469 $sections{$phonenum}{amount} += $amount; #subtotal
4470 $sections{$phonenum}{calls}++;
4471 $sections{$phonenum}{duration} += $detail->duration;
4473 my $desc = $detail->regionname;
4474 my $description = $desc;
4475 $description = substr($desc, 0, $maxlength). '...'
4476 if $format eq 'latex' && length($desc) > $maxlength;
4478 $lines{$phonenum}{$desc} ||= {
4479 description => &{$escape}($description),
4480 #pkgpart => $part_pkg->pkgpart,
4488 product_code => 'N/A',
4489 ext_description => [],
4492 $lines{$phonenum}{$desc}{amount} += $amount;
4493 $lines{$phonenum}{$desc}{calls}++;
4494 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4496 my $line = $usage_class{$detail->classnum}->classname;
4497 $sections{"$phonenum $line"} ||=
4501 'sort_weight' => $usage_class{$detail->classnum}->weight,
4502 'phonenum' => $phonenum,
4503 'header' => [ @header ],
4505 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4506 $sections{"$phonenum $line"}{calls}++;
4507 $sections{"$phonenum $line"}{duration} += $detail->duration;
4509 $lines{"$phonenum $line"}{$desc} ||= {
4510 description => &{$escape}($description),
4511 #pkgpart => $part_pkg->pkgpart,
4519 product_code => 'N/A',
4520 ext_description => [],
4523 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4524 $lines{"$phonenum $line"}{$desc}{calls}++;
4525 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4526 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4527 $detail->formatted('format' => $format);
4532 my %sectionmap = ();
4533 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4534 foreach ( keys %sections ) {
4535 my @header = @{ $sections{$_}{header} || [] };
4537 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4538 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4539 my $usage_class = $summary ? $simple : $usage_simple;
4540 my $ending = $summary ? ' usage charges' : '';
4543 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4545 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4546 'amount' => $sections{$_}{amount}, #subtotal
4547 'calls' => $sections{$_}{calls},
4548 'duration' => $sections{$_}{duration},
4550 'tax_section' => '',
4551 'phonenum' => $sections{$_}{phonenum},
4552 'sort_weight' => $sections{$_}{sort_weight},
4553 'post_total' => $summary, #inspire pagebreak
4555 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4556 qw( description_generator
4559 total_line_generator
4566 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4567 $a->{sort_weight} <=> $b->{sort_weight}
4572 foreach my $section ( keys %lines ) {
4573 foreach my $line ( keys %{$lines{$section}} ) {
4574 my $l = $lines{$section}{$line};
4575 $l->{section} = $sectionmap{$section};
4576 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4577 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4582 if($conf->exists('phone_usage_class_summary')) {
4583 # this only works with Latex
4587 # after this, we'll have only two sections per DID:
4588 # Calls Summary and Calls Detail
4589 foreach my $section ( @sections ) {
4590 if($section->{'post_total'}) {
4591 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4592 $section->{'total_line_generator'} = sub { '' };
4593 $section->{'total_generator'} = sub { '' };
4594 $section->{'header_generator'} = sub { '' };
4595 $section->{'description_generator'} = '';
4596 push @newsections, $section;
4597 my %calls_detail = %$section;
4598 $calls_detail{'post_total'} = '';
4599 $calls_detail{'sort_weight'} = '';
4600 $calls_detail{'description_generator'} = sub { '' };
4601 $calls_detail{'header_generator'} = sub {
4602 return ' & Date/Time & Called Number & Duration & Price'
4603 if $format eq 'latex';
4606 $calls_detail{'description'} = 'Calls Detail: '
4607 . $section->{'phonenum'};
4608 push @newsections, \%calls_detail;
4612 # after this, each usage class is collapsed/summarized into a single
4613 # line under the Calls Summary section
4614 foreach my $newsection ( @newsections ) {
4615 if($newsection->{'post_total'}) { # this means Calls Summary
4616 foreach my $section ( @sections ) {
4617 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4618 && !$section->{'post_total'});
4619 my $newdesc = $section->{'description'};
4620 my $tn = $section->{'phonenum'};
4621 $newdesc =~ s/$tn//g;
4622 my $line = { ext_description => [],
4626 calls => $section->{'calls'},
4627 section => $newsection,
4628 duration => $section->{'duration'},
4629 description => $newdesc,
4630 amount => sprintf("%.2f",$section->{'amount'}),
4631 product_code => 'N/A',
4633 push @newlines, $line;
4638 # after this, Calls Details is populated with all CDRs
4639 foreach my $newsection ( @newsections ) {
4640 if(!$newsection->{'post_total'}) { # this means Calls Details
4641 foreach my $line ( @lines ) {
4642 next unless (scalar(@{$line->{'ext_description'}}) &&
4643 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4645 my @extdesc = @{$line->{'ext_description'}};
4647 foreach my $extdesc ( @extdesc ) {
4648 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4649 push @newextdesc, $extdesc;
4651 $line->{'ext_description'} = \@newextdesc;
4652 $line->{'section'} = $newsection;
4653 push @newlines, $line;
4658 return(\@newsections, \@newlines);
4661 return(\@sections, \@lines);
4665 sub _items { # seems to be unused
4668 #my @display = scalar(@_)
4670 # : qw( _items_previous _items_pkg );
4671 # #: qw( _items_pkg );
4672 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4673 my @display = qw( _items_previous _items_pkg );
4676 foreach my $display ( @display ) {
4677 push @b, $self->$display(@_);
4682 sub _items_previous {
4684 my $conf = $self->conf;
4685 my $cust_main = $self->cust_main;
4686 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4688 foreach ( @pr_cust_bill ) {
4689 my $date = $conf->exists('invoice_show_prior_due_date')
4690 ? 'due '. $_->due_date2str($date_format)
4691 : time2str($date_format, $_->_date);
4693 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4694 #'pkgpart' => 'N/A',
4696 'amount' => sprintf("%.2f", $_->owed),
4702 # 'description' => 'Previous Balance',
4703 # #'pkgpart' => 'N/A',
4704 # 'pkgnum' => 'N/A',
4705 # 'amount' => sprintf("%10.2f", $pr_total ),
4706 # 'ext_description' => [ map {
4707 # "Invoice ". $_->invnum.
4708 # " (". time2str("%x",$_->_date). ") ".
4709 # sprintf("%10.2f", $_->owed)
4710 # } @pr_cust_bill ],
4715 =item _items_pkg [ OPTIONS ]
4717 Return line item hashes for each package item on this invoice. Nearly
4720 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4722 The only OPTIONS accepted is 'section', which may point to a hashref
4723 with a key named 'condensed', which may have a true value. If it
4724 does, this method tries to merge identical items into items with
4725 'quantity' equal to the number of items (not the sum of their
4726 separate quantities, for some reason).
4734 warn "$me _items_pkg searching for all package line items\n"
4737 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4739 warn "$me _items_pkg filtering line items\n"
4741 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4743 if ($options{section} && $options{section}->{condensed}) {
4745 warn "$me _items_pkg condensing section\n"
4749 local $Storable::canonical = 1;
4750 foreach ( @items ) {
4752 delete $item->{ref};
4753 delete $item->{ext_description};
4754 my $key = freeze($item);
4755 $itemshash{$key} ||= 0;
4756 $itemshash{$key} ++; # += $item->{quantity};
4758 @items = sort { $a->{description} cmp $b->{description} }
4759 map { my $i = thaw($_);
4760 $i->{quantity} = $itemshash{$_};
4762 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4768 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4775 return 0 unless $a->itemdesc cmp $b->itemdesc;
4776 return -1 if $b->itemdesc eq 'Tax';
4777 return 1 if $a->itemdesc eq 'Tax';
4778 return -1 if $b->itemdesc eq 'Other surcharges';
4779 return 1 if $a->itemdesc eq 'Other surcharges';
4780 $a->itemdesc cmp $b->itemdesc;
4785 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4786 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4789 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4791 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4792 list of hashrefs describing the line items they generate on the invoice.
4794 OPTIONS may include:
4796 format: the invoice format.
4798 escape_function: the function used to escape strings.
4800 DEPRECATED? (expensive, mostly unused?)
4801 format_function: the function used to format CDRs.
4803 section: a hashref containing 'description'; if this is present,
4804 cust_bill_pkg_display records not belonging to this section are
4807 multisection: a flag indicating that this is a multisection invoice,
4808 which does something complicated.
4810 multilocation: a flag to display the location label for the package.
4812 Returns a list of hashrefs, each of which may contain:
4814 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4815 ext_description, which is an arrayref of detail lines to show below
4820 sub _items_cust_bill_pkg {
4822 my $conf = $self->conf;
4823 my $cust_bill_pkgs = shift;
4826 my $format = $opt{format} || '';
4827 my $escape_function = $opt{escape_function} || sub { shift };
4828 my $format_function = $opt{format_function} || '';
4829 my $no_usage = $opt{no_usage} || '';
4830 my $unsquelched = $opt{unsquelched} || ''; #unused
4831 my $section = $opt{section}->{description} if $opt{section};
4832 my $summary_page = $opt{summary_page} || ''; #unused
4833 my $multilocation = $opt{multilocation} || '';
4834 my $multisection = $opt{multisection} || '';
4835 my $discount_show_always = 0;
4837 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4840 my ($s, $r, $u) = ( undef, undef, undef );
4841 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4844 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4845 if ( $_ && !$cust_bill_pkg->hidden ) {
4846 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4847 $_->{amount} =~ s/^\-0\.00$/0.00/;
4848 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4850 if $_->{amount} != 0
4851 || $discount_show_always
4852 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4853 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4859 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4860 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4863 foreach my $display ( grep { defined($section)
4864 ? $_->section eq $section
4867 #grep { !$_->summary || !$summary_page } # bunk!
4868 grep { !$_->summary || $multisection }
4869 $cust_bill_pkg->cust_bill_pkg_display
4873 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4874 $display->billpkgdisplaynum. "\n"
4877 my $type = $display->type;
4879 my $desc = $cust_bill_pkg->desc;
4880 $desc = substr($desc, 0, $maxlength). '...'
4881 if $format eq 'latex' && length($desc) > $maxlength;
4883 my %details_opt = ( 'format' => $format,
4884 'escape_function' => $escape_function,
4885 'format_function' => $format_function,
4886 'no_usage' => $opt{'no_usage'},
4889 if ( $cust_bill_pkg->pkgnum > 0 ) {
4891 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4894 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4896 # start/end dates for invoice formats that do nonstandard
4898 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4900 if ( (!$type || $type eq 'S')
4901 && ( $cust_bill_pkg->setup != 0
4902 || $cust_bill_pkg->setup_show_zero
4907 warn "$me _items_cust_bill_pkg adding setup\n"
4910 my $description = $desc;
4911 $description .= ' Setup'
4912 if $cust_bill_pkg->recur != 0
4913 || $discount_show_always
4914 || $cust_bill_pkg->recur_show_zero;
4917 unless ( $cust_pkg->part_pkg->hide_svc_detail
4918 || $cust_bill_pkg->hidden )
4921 push @d, map &{$escape_function}($_),
4922 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4923 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4925 if ( $multilocation ) {
4926 my $loc = $cust_pkg->location_label;
4927 $loc = substr($loc, 0, $maxlength). '...'
4928 if $format eq 'latex' && length($loc) > $maxlength;
4929 push @d, &{$escape_function}($loc);
4932 } #unless hiding service details
4934 push @d, $cust_bill_pkg->details(%details_opt)
4935 if $cust_bill_pkg->recur == 0;
4937 if ( $cust_bill_pkg->hidden ) {
4938 $s->{amount} += $cust_bill_pkg->setup;
4939 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4940 push @{ $s->{ext_description} }, @d;
4944 description => $description,
4945 #pkgpart => $part_pkg->pkgpart,
4946 pkgnum => $cust_bill_pkg->pkgnum,
4947 amount => $cust_bill_pkg->setup,
4948 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4949 unit_amount => $cust_bill_pkg->unitsetup,
4950 quantity => $cust_bill_pkg->quantity,
4951 ext_description => \@d,
4957 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4959 $cust_bill_pkg->recur != 0
4960 || $cust_bill_pkg->setup == 0
4961 || $discount_show_always
4962 || $cust_bill_pkg->recur_show_zero
4967 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4970 my $is_summary = $display->summary;
4971 my $description = ($is_summary && $type && $type eq 'U')
4972 ? "Usage charges" : $desc;
4974 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4975 " - ". time2str($date_format, $cust_bill_pkg->edate).
4977 unless $conf->exists('disable_line_item_date_ranges')
4978 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4981 my @seconds = (); # for display of usage info
4983 #at least until cust_bill_pkg has "past" ranges in addition to
4984 #the "future" sdate/edate ones... see #3032
4985 my @dates = ( $self->_date );
4986 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4987 push @dates, $prev->sdate if $prev;
4988 push @dates, undef if !$prev;
4990 unless ( $cust_pkg->part_pkg->hide_svc_detail
4991 || $cust_bill_pkg->itemdesc
4992 || $cust_bill_pkg->hidden
4993 || $is_summary && $type && $type eq 'U' )
4996 warn "$me _items_cust_bill_pkg adding service details\n"
4999 push @d, map &{$escape_function}($_),
5000 $cust_pkg->h_labels_short(@dates, 'I')
5001 #$cust_bill_pkg->edate,
5002 #$cust_bill_pkg->sdate)
5003 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
5005 warn "$me _items_cust_bill_pkg done adding service details\n"
5008 if ( $multilocation ) {
5009 my $loc = $cust_pkg->location_label;
5010 $loc = substr($loc, 0, $maxlength). '...'
5011 if $format eq 'latex' && length($loc) > $maxlength;
5012 push @d, &{$escape_function}($loc);
5015 # Display of seconds_since_sqlradacct:
5016 # On the invoice, when processing @detail_items, look for a field
5017 # named 'seconds'. This will contain total seconds for each
5018 # service, in the same order as @ext_description. For services
5019 # that don't support this it will show undef.
5020 if ( $conf->exists('svc_acct-usage_seconds')
5021 and ! $cust_bill_pkg->pkgpart_override ) {
5022 foreach my $cust_svc (
5023 $cust_pkg->h_cust_svc(@dates, 'I')
5026 # eval because not having any part_export_usage exports
5027 # is a fatal error, last_bill/_date because that's how
5028 # sqlradius_hour billing does it
5030 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5032 push @seconds, $sec;
5034 } #if svc_acct-usage_seconds
5038 unless ( $is_summary ) {
5039 warn "$me _items_cust_bill_pkg adding details\n"
5042 #instead of omitting details entirely in this case (unwanted side
5043 # effects), just omit CDRs
5044 $details_opt{'no_usage'} = 1
5045 if $type && $type eq 'R';
5047 push @d, $cust_bill_pkg->details(%details_opt);
5050 warn "$me _items_cust_bill_pkg calculating amount\n"
5055 $amount = $cust_bill_pkg->recur;
5056 } elsif ($type eq 'R') {
5057 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5058 } elsif ($type eq 'U') {
5059 $amount = $cust_bill_pkg->usage;
5062 if ( !$type || $type eq 'R' ) {
5064 warn "$me _items_cust_bill_pkg adding recur\n"
5067 if ( $cust_bill_pkg->hidden ) {
5068 $r->{amount} += $amount;
5069 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5070 push @{ $r->{ext_description} }, @d;
5073 description => $description,
5074 #pkgpart => $part_pkg->pkgpart,
5075 pkgnum => $cust_bill_pkg->pkgnum,
5077 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5078 unit_amount => $cust_bill_pkg->unitrecur,
5079 quantity => $cust_bill_pkg->quantity,
5081 ext_description => \@d,
5083 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5086 } else { # $type eq 'U'
5088 warn "$me _items_cust_bill_pkg adding usage\n"
5091 if ( $cust_bill_pkg->hidden ) {
5092 $u->{amount} += $amount;
5093 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5094 push @{ $u->{ext_description} }, @d;
5097 description => $description,
5098 #pkgpart => $part_pkg->pkgpart,
5099 pkgnum => $cust_bill_pkg->pkgnum,
5101 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5102 unit_amount => $cust_bill_pkg->unitrecur,
5103 quantity => $cust_bill_pkg->quantity,
5105 ext_description => \@d,
5110 } # recurring or usage with recurring charge
5112 } else { #pkgnum tax or one-shot line item (??)
5114 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5117 if ( $cust_bill_pkg->setup != 0 ) {
5119 'description' => $desc,
5120 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5123 if ( $cust_bill_pkg->recur != 0 ) {
5125 'description' => "$desc (".
5126 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5127 time2str($date_format, $cust_bill_pkg->edate). ')',
5128 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5136 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5137 && $conf->exists('discount-show-always'));
5141 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5143 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5144 $_->{amount} =~ s/^\-0\.00$/0.00/;
5145 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5147 if $_->{amount} != 0
5148 || $discount_show_always
5149 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5150 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5154 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5161 sub _items_credits {
5162 my( $self, %opt ) = @_;
5163 my $trim_len = $opt{'trim_len'} || 60;
5167 foreach ( $self->cust_credited ) {
5169 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5171 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5172 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5173 $reason = " ($reason) " if $reason;
5176 #'description' => 'Credit ref\#'. $_->crednum.
5177 # " (". time2str("%x",$_->cust_credit->_date) .")".
5179 'description' => $self->mt('Credit applied').' '.
5180 time2str($date_format,$_->cust_credit->_date). $reason,
5181 'amount' => sprintf("%.2f",$_->amount),
5189 sub _items_payments {
5193 #get & print payments
5194 foreach ( $self->cust_bill_pay ) {
5196 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5199 'description' => $self->mt('Payment received').' '.
5200 time2str($date_format,$_->cust_pay->_date ),
5201 'amount' => sprintf("%.2f", $_->amount )
5209 =item _items_discounts_avail
5211 Returns an array of line item hashrefs representing available term discounts
5212 for this invoice. This makes the same assumptions that apply to term
5213 discounts in general: that the package is billed monthly, at a flat rate,
5214 with no usage charges. A prorated first month will be handled, as will
5215 a setup fee if the discount is allowed to apply to setup fees.
5219 sub _items_discounts_avail {
5222 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5224 my ($previous_balance) = $self->previous;
5226 foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5227 $terms{$_->months} = {
5229 base => $previous_balance || 0, # pre-discount sum of charges
5230 discounted => $previous_balance || 0, # post-discount sum
5231 list_pkgnums => 0, # whether any packages are not discounted
5234 foreach my $months (keys %terms) {
5235 my $hash = $terms{$months};
5237 # tricky, because packages may not all be eligible for the same discounts
5238 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5239 my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5240 my $part_pkg = $cust_pkg->part_pkg or next;
5241 my $freq = $part_pkg->freq;
5242 my $setup = $cust_bill_pkg->setup || 0;
5243 my $recur = $cust_bill_pkg->recur || 0;
5245 if ( $freq eq '1' ) { #monthly
5246 my $permonth = $part_pkg->base_recur_permonth || 0;
5248 my ($discount) = grep { $_->months == $months }
5249 map { $_->discount } $part_pkg->part_pkg_discount;
5251 $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5256 if ( $discount->setup ) {
5257 $discountable += $setup;
5260 $hash->{discounted} += $setup;
5263 if ( $discount->percent ) {
5264 $discountable += $months * $permonth;
5265 $discountable -= ($discountable * $discount->percent / 100);
5266 $discountable -= ($permonth - $recur); # correct for prorate
5267 $hash->{discounted} += $discountable;
5270 $discountable += $recur;
5271 $discountable -= $discount->amount * $recur/$permonth;
5273 $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5276 $hash->{discounted} += $discountable;
5277 push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5280 $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5281 $hash->{list_pkgnums} = 1;
5284 else { # all non-monthly packages: include current charges only
5285 $hash->{discounted} += $setup + $recur;
5286 $hash->{base} += $setup + $recur;
5287 $hash->{list_pkgnums} = 1;
5289 } #foreach $cust_bill_pkg
5291 # don't show this line if no packages have discounts at this term
5292 # or if there are no new charges to apply the discount to
5293 delete $terms{$months} if $hash->{base} == $hash->{discounted}
5294 or $hash->{base} == 0;
5298 $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5300 foreach my $months (keys %terms) {
5301 my $hash = $terms{$months};
5302 my $term_total = sprintf('%.2f', $hash->{discounted});
5303 # possibly shouldn't include previous balance in these?
5304 my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5305 my $permonth = sprintf('%.2f', $term_total / $months);
5307 $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5310 $hash->{amount} = $self->mt('[_1] ([_2] per month)',
5311 $term_total, $money_char.$permonth
5315 if ( $list_pkgnums ) {
5316 push @detail, $self->mt('discount on item'). ' '.
5317 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5319 $hash->{ext_description} = join ', ', @detail;
5322 map { $terms{$_} } sort {$b <=> $a} keys %terms;
5325 =item call_details [ OPTION => VALUE ... ]
5327 Returns an array of CSV strings representing the call details for this invoice
5328 The only option available is the boolean prepend_billed_number
5333 my ($self, %opt) = @_;
5335 my $format_function = sub { shift };
5337 if ($opt{prepend_billed_number}) {
5338 $format_function = sub {
5342 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5347 my @details = map { $_->details( 'format_function' => $format_function,
5348 'escape_function' => sub{ return() },
5352 $self->cust_bill_pkg;
5353 my $header = $details[0];
5354 ( $header, grep { $_ ne $header } @details );
5364 =item process_reprint
5368 sub process_reprint {
5369 process_re_X('print', @_);
5372 =item process_reemail
5376 sub process_reemail {
5377 process_re_X('email', @_);
5385 process_re_X('fax', @_);
5393 process_re_X('ftp', @_);
5400 sub process_respool {
5401 process_re_X('spool', @_);
5404 use Storable qw(thaw);
5408 my( $method, $job ) = ( shift, shift );
5409 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5411 my $param = thaw(decode_base64(shift));
5412 warn Dumper($param) if $DEBUG;
5423 my($method, $job, %param ) = @_;
5425 warn "re_X $method for job $job with param:\n".
5426 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5429 #some false laziness w/search/cust_bill.html
5431 my $orderby = 'ORDER BY cust_bill._date';
5433 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5435 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5437 my @cust_bill = qsearch( {
5438 #'select' => "cust_bill.*",
5439 'table' => 'cust_bill',
5440 'addl_from' => $addl_from,
5442 'extra_sql' => $extra_sql,
5443 'order_by' => $orderby,
5447 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5449 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5452 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5453 foreach my $cust_bill ( @cust_bill ) {
5454 $cust_bill->$method();
5456 if ( $job ) { #progressbar foo
5458 if ( time - $min_sec > $last ) {
5459 my $error = $job->update_statustext(
5460 int( 100 * $num / scalar(@cust_bill) )
5462 die $error if $error;
5473 =head1 CLASS METHODS
5479 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5484 my ($class, $start, $end) = @_;
5486 $class->paid_sql($start, $end). ' - '.
5487 $class->credited_sql($start, $end);
5492 Returns an SQL fragment to retreive the net amount (charged minus credited).
5497 my ($class, $start, $end) = @_;
5498 'charged - '. $class->credited_sql($start, $end);
5503 Returns an SQL fragment to retreive the amount paid against this invoice.
5508 my ($class, $start, $end) = @_;
5509 $start &&= "AND cust_bill_pay._date <= $start";
5510 $end &&= "AND cust_bill_pay._date > $end";
5511 $start = '' unless defined($start);
5512 $end = '' unless defined($end);
5513 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5514 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5519 Returns an SQL fragment to retreive the amount credited against this invoice.
5524 my ($class, $start, $end) = @_;
5525 $start &&= "AND cust_credit_bill._date <= $start";
5526 $end &&= "AND cust_credit_bill._date > $end";
5527 $start = '' unless defined($start);
5528 $end = '' unless defined($end);
5529 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5530 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5535 Returns an SQL fragment to retrieve the due date of an invoice.
5536 Currently only supported on PostgreSQL.
5541 my $conf = new FS::Conf;
5545 cust_bill.invoice_terms,
5546 cust_main.invoice_terms,
5547 \''.($conf->config('invoice_default_terms') || '').'\'
5548 ), E\'Net (\\\\d+)\'
5550 ) * 86400 + cust_bill._date'
5553 =item search_sql_where HASHREF
5555 Class method which returns an SQL WHERE fragment to search for parameters
5556 specified in HASHREF. Valid parameters are
5562 List reference of start date, end date, as UNIX timestamps.
5572 List reference of charged limits (exclusive).
5576 List reference of charged limits (exclusive).
5580 flag, return open invoices only
5584 flag, return net invoices only
5588 =item newest_percust
5592 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5596 sub search_sql_where {
5597 my($class, $param) = @_;
5599 warn "$me search_sql_where called with params: \n".
5600 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5606 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5607 push @search, "cust_main.agentnum = $1";
5611 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5612 push @search, "cust_bill.custnum = $1";
5616 if ( $param->{_date} ) {
5617 my($beginning, $ending) = @{$param->{_date}};
5619 push @search, "cust_bill._date >= $beginning",
5620 "cust_bill._date < $ending";
5624 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5625 push @search, "cust_bill.invnum >= $1";
5627 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5628 push @search, "cust_bill.invnum <= $1";
5632 if ( $param->{charged} ) {
5633 my @charged = ref($param->{charged})
5634 ? @{ $param->{charged} }
5635 : ($param->{charged});
5637 push @search, map { s/^charged/cust_bill.charged/; $_; }
5641 my $owed_sql = FS::cust_bill->owed_sql;
5644 if ( $param->{owed} ) {
5645 my @owed = ref($param->{owed})
5646 ? @{ $param->{owed} }
5648 push @search, map { s/^owed/$owed_sql/; $_; }
5653 push @search, "0 != $owed_sql"
5654 if $param->{'open'};
5655 push @search, '0 != '. FS::cust_bill->net_sql
5659 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5660 if $param->{'days'};
5663 if ( $param->{'newest_percust'} ) {
5665 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5666 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5668 my @newest_where = map { my $x = $_;
5669 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5672 grep ! /^cust_main./, @search;
5673 my $newest_where = scalar(@newest_where)
5674 ? ' AND '. join(' AND ', @newest_where)
5678 push @search, "cust_bill._date = (
5679 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5680 WHERE newest_cust_bill.custnum = cust_bill.custnum
5686 #agent virtualization
5687 my $curuser = $FS::CurrentUser::CurrentUser;
5688 if ( $curuser->username eq 'fs_queue'
5689 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5691 my $newuser = qsearchs('access_user', {
5692 'username' => $username,
5696 $curuser = $newuser;
5698 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5701 push @search, $curuser->agentnums_sql;
5703 join(' AND ', @search );
5715 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5716 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base