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',
1048 $args{'from'} =~ /\@([\w\.\-]+)/;
1049 my $from = $1 || 'example.com';
1050 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1053 my $agentnum = $cust_main->agentnum;
1054 if ( defined($args{'template'}) && length($args{'template'})
1055 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1058 $logo = 'logo_'. $args{'template'}. '.png';
1062 my $image_data = $conf->config_binary( $logo, $agentnum);
1064 my $image = build MIME::Entity
1065 'Type' => 'image/png',
1066 'Encoding' => 'base64',
1067 'Data' => $image_data,
1068 'Filename' => 'logo.png',
1069 'Content-ID' => "<$content_id>",
1073 if($conf->exists('invoice-barcode')){
1074 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1075 $barcode = build MIME::Entity
1076 'Type' => 'image/png',
1077 'Encoding' => 'base64',
1078 'Data' => $self->invoice_barcode(0),
1079 'Filename' => 'barcode.png',
1080 'Content-ID' => "<$barcode_content_id>",
1082 $opt{'barcode_cid'} = $barcode_content_id;
1085 $alternative->attach(
1086 'Type' => 'text/html',
1087 'Encoding' => 'quoted-printable',
1088 'Data' => [ '<html>',
1091 ' '. encode_entities($return{'subject'}),
1094 ' <body bgcolor="#e8e8e8">',
1095 $self->print_html({ 'cid'=>$content_id, %opt }),
1099 'Disposition' => 'inline',
1100 #'Filename' => 'invoice.pdf',
1103 my @otherparts = ();
1104 if ( $cust_main->email_csv_cdr ) {
1106 push @otherparts, build MIME::Entity
1107 'Type' => 'text/csv',
1108 'Encoding' => '7bit',
1109 'Data' => [ map { "$_\n" }
1110 $self->call_details('prepend_billed_number' => 1)
1112 'Disposition' => 'attachment',
1113 'Filename' => 'usage-'. $self->invnum. '.csv',
1118 if ( $conf->exists('invoice_email_pdf') ) {
1123 # multipart/alternative
1129 my $related = build MIME::Entity 'Type' => 'multipart/related',
1130 'Encoding' => '7bit';
1132 #false laziness w/Misc::send_email
1133 $related->head->replace('Content-type',
1134 $related->mime_type.
1135 '; boundary="'. $related->head->multipart_boundary. '"'.
1136 '; type=multipart/alternative'
1139 $related->add_part($alternative);
1141 $related->add_part($image);
1143 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1145 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1149 #no other attachment:
1151 # multipart/alternative
1156 $return{'content-type'} = 'multipart/related';
1157 if($conf->exists('invoice-barcode')){
1158 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1161 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1163 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1164 #$return{'disposition'} = 'inline';
1170 if ( $conf->exists('invoice_email_pdf') ) {
1171 warn "$me creating PDF attachment"
1174 #mime parts arguments a la MIME::Entity->build().
1175 $return{'mimeparts'} = [
1176 { $self->mimebuild_pdf(\%opt) }
1180 if ( $conf->exists('invoice_email_pdf')
1181 and scalar($conf->config('invoice_email_pdf_note')) ) {
1183 warn "$me using 'invoice_email_pdf_note'"
1185 $return{'body'} = [ map { $_ . "\n" }
1186 $conf->config('invoice_email_pdf_note')
1191 warn "$me not using 'invoice_email_pdf_note'"
1193 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1194 $return{'body'} = $args{'print_text'};
1196 $return{'body'} = [ $self->print_text(\%opt) ];
1209 Returns a list suitable for passing to MIME::Entity->build(), representing
1210 this invoice as PDF attachment.
1217 'Type' => 'application/pdf',
1218 'Encoding' => 'base64',
1219 'Data' => [ $self->print_pdf(@_) ],
1220 'Disposition' => 'attachment',
1221 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1225 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1227 Sends this invoice to the destinations configured for this customer: sends
1228 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1230 Options can be passed as a hashref (recommended) or as a list of up to
1231 four values for templatename, agentnum, invoice_from and amount.
1233 I<template>, if specified, is the name of a suffix for alternate invoices.
1235 I<agentnum>, if specified, means that this invoice will only be sent for customers
1236 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1237 single agent) or an arrayref of agentnums.
1239 I<invoice_from>, if specified, overrides the default email invoice From: address.
1241 I<amount>, if specified, only sends the invoice if the total amount owed on this
1242 invoice and all older invoices is greater than the specified amount.
1244 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1248 sub queueable_send {
1251 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1252 or die "invalid invoice number: " . $opt{invnum};
1254 my @args = ( $opt{template}, $opt{agentnum} );
1255 push @args, $opt{invoice_from}
1256 if exists($opt{invoice_from}) && $opt{invoice_from};
1258 my $error = $self->send( @args );
1259 die $error if $error;
1265 my $conf = $self->conf;
1267 my( $template, $invoice_from, $notice_name );
1269 my $balance_over = 0;
1273 $template = $opt->{'template'} || '';
1274 if ( $agentnums = $opt->{'agentnum'} ) {
1275 $agentnums = [ $agentnums ] unless ref($agentnums);
1277 $invoice_from = $opt->{'invoice_from'};
1278 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1279 $notice_name = $opt->{'notice_name'};
1281 $template = scalar(@_) ? shift : '';
1282 if ( scalar(@_) && $_[0] ) {
1283 $agentnums = ref($_[0]) ? shift : [ shift ];
1285 $invoice_from = shift if scalar(@_);
1286 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1289 return 'N/A' unless ! $agentnums
1290 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1293 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1295 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1296 $conf->config('invoice_from', $self->cust_main->agentnum );
1299 'template' => $template,
1300 'invoice_from' => $invoice_from,
1301 'notice_name' => ( $notice_name || 'Invoice' ),
1304 my @invoicing_list = $self->cust_main->invoicing_list;
1306 #$self->email_invoice(\%opt)
1308 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1310 #$self->print_invoice(\%opt)
1312 if grep { $_ eq 'POST' } @invoicing_list; #postal
1314 $self->fax_invoice(\%opt)
1315 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1321 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1323 Emails this invoice.
1325 Options can be passed as a hashref (recommended) or as a list of up to
1326 two values for templatename and invoice_from.
1328 I<template>, if specified, is the name of a suffix for alternate invoices.
1330 I<invoice_from>, if specified, overrides the default email invoice From: address.
1332 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1336 sub queueable_email {
1339 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1340 or die "invalid invoice number: " . $opt{invnum};
1342 my %args = ( 'template' => $opt{template} );
1343 $args{$_} = $opt{$_}
1344 foreach grep { exists($opt{$_}) && $opt{$_} }
1345 qw( invoice_from notice_name no_coupon );
1347 my $error = $self->email( \%args );
1348 die $error if $error;
1352 #sub email_invoice {
1355 return if $self->hide;
1356 my $conf = $self->conf;
1358 my( $template, $invoice_from, $notice_name, $no_coupon );
1361 $template = $opt->{'template'} || '';
1362 $invoice_from = $opt->{'invoice_from'};
1363 $notice_name = $opt->{'notice_name'} || 'Invoice';
1364 $no_coupon = $opt->{'no_coupon'} || 0;
1366 $template = scalar(@_) ? shift : '';
1367 $invoice_from = shift if scalar(@_);
1368 $notice_name = 'Invoice';
1372 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1373 $conf->config('invoice_from', $self->cust_main->agentnum );
1375 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1376 $self->cust_main->invoicing_list;
1378 if ( ! @invoicing_list ) { #no recipients
1379 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1380 die 'No recipients for customer #'. $self->custnum;
1382 #default: better to notify this person than silence
1383 @invoicing_list = ($invoice_from);
1387 my $subject = $self->email_subject($template);
1389 my $error = send_email(
1390 $self->generate_email(
1391 'from' => $invoice_from,
1392 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1393 'subject' => $subject,
1394 'template' => $template,
1395 'notice_name' => $notice_name,
1396 'no_coupon' => $no_coupon,
1399 die "can't email invoice: $error\n" if $error;
1400 #die "$error\n" if $error;
1406 my $conf = $self->conf;
1408 #my $template = scalar(@_) ? shift : '';
1411 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1414 my $cust_main = $self->cust_main;
1415 my $name = $cust_main->name;
1416 my $name_short = $cust_main->name_short;
1417 my $invoice_number = $self->invnum;
1418 my $invoice_date = $self->_date_pretty;
1420 eval qq("$subject");
1423 =item lpr_data HASHREF | [ TEMPLATE ]
1425 Returns the postscript or plaintext for this invoice as an arrayref.
1427 Options can be passed as a hashref (recommended) or as a single optional value
1430 I<template>, if specified, is the name of a suffix for alternate invoices.
1432 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1438 my $conf = $self->conf;
1439 my( $template, $notice_name );
1442 $template = $opt->{'template'} || '';
1443 $notice_name = $opt->{'notice_name'} || 'Invoice';
1445 $template = scalar(@_) ? shift : '';
1446 $notice_name = 'Invoice';
1450 'template' => $template,
1451 'notice_name' => $notice_name,
1454 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1455 [ $self->$method( \%opt ) ];
1458 =item print HASHREF | [ TEMPLATE ]
1460 Prints this invoice.
1462 Options can be passed as a hashref (recommended) or as a single optional
1465 I<template>, if specified, is the name of a suffix for alternate invoices.
1467 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1471 #sub print_invoice {
1474 return if $self->hide;
1475 my $conf = $self->conf;
1477 my( $template, $notice_name );
1480 $template = $opt->{'template'} || '';
1481 $notice_name = $opt->{'notice_name'} || 'Invoice';
1483 $template = scalar(@_) ? shift : '';
1484 $notice_name = 'Invoice';
1488 'template' => $template,
1489 'notice_name' => $notice_name,
1492 if($conf->exists('invoice_print_pdf')) {
1493 # Add the invoice to the current batch.
1494 $self->batch_invoice(\%opt);
1497 do_print $self->lpr_data(\%opt);
1501 =item fax_invoice HASHREF | [ TEMPLATE ]
1505 Options can be passed as a hashref (recommended) or as a single optional
1508 I<template>, if specified, is the name of a suffix for alternate invoices.
1510 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1516 return if $self->hide;
1517 my $conf = $self->conf;
1519 my( $template, $notice_name );
1522 $template = $opt->{'template'} || '';
1523 $notice_name = $opt->{'notice_name'} || 'Invoice';
1525 $template = scalar(@_) ? shift : '';
1526 $notice_name = 'Invoice';
1529 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1530 unless $conf->exists('invoice_latex');
1532 my $dialstring = $self->cust_main->getfield('fax');
1536 'template' => $template,
1537 'notice_name' => $notice_name,
1540 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1541 'dialstring' => $dialstring,
1543 die $error if $error;
1547 =item batch_invoice [ HASHREF ]
1549 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1550 isn't an open batch, one will be created.
1555 my ($self, $opt) = @_;
1556 my $bill_batch = $self->get_open_bill_batch;
1557 my $cust_bill_batch = FS::cust_bill_batch->new({
1558 batchnum => $bill_batch->batchnum,
1559 invnum => $self->invnum,
1561 return $cust_bill_batch->insert($opt);
1564 =item get_open_batch
1566 Returns the currently open batch as an FS::bill_batch object, creating a new
1567 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1572 sub get_open_bill_batch {
1574 my $conf = $self->conf;
1575 my $hashref = { status => 'O' };
1576 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1577 ? $self->cust_main->agentnum
1579 my $batch = qsearchs('bill_batch', $hashref);
1580 return $batch if $batch;
1581 $batch = FS::bill_batch->new($hashref);
1582 my $error = $batch->insert;
1583 die $error if $error;
1587 =item ftp_invoice [ TEMPLATENAME ]
1589 Sends this invoice data via FTP.
1591 TEMPLATENAME is unused?
1597 my $conf = $self->conf;
1598 my $template = scalar(@_) ? shift : '';
1601 'protocol' => 'ftp',
1602 'server' => $conf->config('cust_bill-ftpserver'),
1603 'username' => $conf->config('cust_bill-ftpusername'),
1604 'password' => $conf->config('cust_bill-ftppassword'),
1605 'dir' => $conf->config('cust_bill-ftpdir'),
1606 'format' => $conf->config('cust_bill-ftpformat'),
1610 =item spool_invoice [ TEMPLATENAME ]
1612 Spools this invoice data (see L<FS::spool_csv>)
1614 TEMPLATENAME is unused?
1620 my $conf = $self->conf;
1621 my $template = scalar(@_) ? shift : '';
1624 'format' => $conf->config('cust_bill-spoolformat'),
1625 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1629 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1631 Like B<send>, but only sends the invoice if it is the newest open invoice for
1636 sub send_if_newest {
1641 grep { $_->owed > 0 }
1642 qsearch('cust_bill', {
1643 'custnum' => $self->custnum,
1644 #'_date' => { op=>'>', value=>$self->_date },
1645 'invnum' => { op=>'>', value=>$self->invnum },
1652 =item send_csv OPTION => VALUE, ...
1654 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1658 protocol - currently only "ftp"
1664 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1665 and YYMMDDHHMMSS is a timestamp.
1667 See L</print_csv> for a description of the output format.
1672 my($self, %opt) = @_;
1676 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1677 mkdir $spooldir, 0700 unless -d $spooldir;
1679 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1680 my $file = "$spooldir/$tracctnum.csv";
1682 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1684 open(CSV, ">$file") or die "can't open $file: $!";
1692 if ( $opt{protocol} eq 'ftp' ) {
1693 eval "use Net::FTP;";
1695 $net = Net::FTP->new($opt{server}) or die @$;
1697 die "unknown protocol: $opt{protocol}";
1700 $net->login( $opt{username}, $opt{password} )
1701 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1703 $net->binary or die "can't set binary mode";
1705 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1707 $net->put($file) or die "can't put $file: $!";
1717 Spools CSV invoice data.
1723 =item format - 'default' or 'billco'
1725 =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>).
1727 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1729 =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.
1736 my($self, %opt) = @_;
1738 my $cust_main = $self->cust_main;
1740 if ( $opt{'dest'} ) {
1741 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1742 $cust_main->invoicing_list;
1743 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1744 || ! keys %invoicing_list;
1747 if ( $opt{'balanceover'} ) {
1749 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1752 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1753 mkdir $spooldir, 0700 unless -d $spooldir;
1755 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1759 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1760 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1763 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1765 open(CSV, ">>$file") or die "can't open $file: $!";
1766 flock(CSV, LOCK_EX);
1771 if ( lc($opt{'format'}) eq 'billco' ) {
1773 flock(CSV, LOCK_UN);
1778 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1781 open(CSV,">>$file") or die "can't open $file: $!";
1782 flock(CSV, LOCK_EX);
1788 flock(CSV, LOCK_UN);
1795 =item print_csv OPTION => VALUE, ...
1797 Returns CSV data for this invoice.
1801 format - 'default' or 'billco'
1803 Returns a list consisting of two scalars. The first is a single line of CSV
1804 header information for this invoice. The second is one or more lines of CSV
1805 detail information for this invoice.
1807 If I<format> is not specified or "default", the fields of the CSV file are as
1810 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1814 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1816 B<record_type> is C<cust_bill> for the initial header line only. The
1817 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1818 fields are filled in.
1820 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1821 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1824 =item invnum - invoice number
1826 =item custnum - customer number
1828 =item _date - invoice date
1830 =item charged - total invoice amount
1832 =item first - customer first name
1834 =item last - customer first name
1836 =item company - company name
1838 =item address1 - address line 1
1840 =item address2 - address line 1
1850 =item pkg - line item description
1852 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1854 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1856 =item sdate - start date for recurring fee
1858 =item edate - end date for recurring fee
1862 If I<format> is "billco", the fields of the header CSV file are as follows:
1864 +-------------------------------------------------------------------+
1865 | FORMAT HEADER FILE |
1866 |-------------------------------------------------------------------|
1867 | Field | Description | Name | Type | Width |
1868 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1869 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1870 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1871 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1872 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1873 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1874 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1875 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1876 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1877 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1878 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1879 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1880 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1881 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1882 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1883 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1884 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1885 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1886 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1887 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1888 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1889 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1890 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1891 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1892 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1893 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1894 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1895 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1896 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1897 +-------+-------------------------------+------------+------+-------+
1899 If I<format> is "billco", the fields of the detail CSV file are as follows:
1901 FORMAT FOR DETAIL FILE
1903 Field | Description | Name | Type | Width
1904 1 | N/A-Leave Empty | RC | CHAR | 2
1905 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1906 3 | Account Number | TRACCTNUM | CHAR | 15
1907 4 | Invoice Number | TRINVOICE | CHAR | 15
1908 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1909 6 | Transaction Detail | DETAILS | CHAR | 100
1910 7 | Amount | AMT | NUM* | 9
1911 8 | Line Format Control** | LNCTRL | CHAR | 2
1912 9 | Grouping Code | GROUP | CHAR | 2
1913 10 | User Defined | ACCT CODE | CHAR | 15
1918 my($self, %opt) = @_;
1920 eval "use Text::CSV_XS";
1923 my $cust_main = $self->cust_main;
1925 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1927 if ( lc($opt{'format'}) eq 'billco' ) {
1930 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1932 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1934 my( $previous_balance, @unused ) = $self->previous; #previous balance
1936 my $pmt_cr_applied = 0;
1937 $pmt_cr_applied += $_->{'amount'}
1938 foreach ( $self->_items_payments, $self->_items_credits ) ;
1940 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1943 '', # 1 | N/A-Leave Empty CHAR 2
1944 '', # 2 | N/A-Leave Empty CHAR 15
1945 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1946 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1947 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1948 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1949 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1950 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1951 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1952 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1953 '', # 10 | Ancillary Billing Information CHAR 30
1954 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1955 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1958 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1961 $duedate, # 14 | Bill Due Date CHAR 10
1963 $previous_balance, # 15 | Previous Balance NUM* 9
1964 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1965 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1966 $totaldue, # 18 | Total Amt Due NUM* 9
1967 $totaldue, # 19 | Total Amt Due NUM* 9
1968 '', # 20 | 30 Day Aging NUM* 9
1969 '', # 21 | 60 Day Aging NUM* 9
1970 '', # 22 | 90 Day Aging NUM* 9
1971 'N', # 23 | Y/N CHAR 1
1972 '', # 24 | Remittance automation CHAR 100
1973 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1974 $self->custnum, # 26 | Customer Reference Number CHAR 15
1975 '0', # 27 | Federal Tax*** NUM* 9
1976 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1977 '0', # 29 | Other Taxes & Fees*** NUM* 9
1986 time2str("%x", $self->_date),
1987 sprintf("%.2f", $self->charged),
1988 ( map { $cust_main->getfield($_) }
1989 qw( first last company address1 address2 city state zip country ) ),
1991 ) or die "can't create csv";
1994 my $header = $csv->string. "\n";
1997 if ( lc($opt{'format'}) eq 'billco' ) {
2000 foreach my $item ( $self->_items_pkg ) {
2003 '', # 1 | N/A-Leave Empty CHAR 2
2004 '', # 2 | N/A-Leave Empty CHAR 15
2005 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
2006 $self->invnum, # 4 | Invoice Number CHAR 15
2007 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
2008 $item->{'description'}, # 6 | Transaction Detail CHAR 100
2009 $item->{'amount'}, # 7 | Amount NUM* 9
2010 '', # 8 | Line Format Control** CHAR 2
2011 '', # 9 | Grouping Code CHAR 2
2012 '', # 10 | User Defined CHAR 15
2015 $detail .= $csv->string. "\n";
2021 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2023 my($pkg, $setup, $recur, $sdate, $edate);
2024 if ( $cust_bill_pkg->pkgnum ) {
2026 ($pkg, $setup, $recur, $sdate, $edate) = (
2027 $cust_bill_pkg->part_pkg->pkg,
2028 ( $cust_bill_pkg->setup != 0
2029 ? sprintf("%.2f", $cust_bill_pkg->setup )
2031 ( $cust_bill_pkg->recur != 0
2032 ? sprintf("%.2f", $cust_bill_pkg->recur )
2034 ( $cust_bill_pkg->sdate
2035 ? time2str("%x", $cust_bill_pkg->sdate)
2037 ($cust_bill_pkg->edate
2038 ?time2str("%x", $cust_bill_pkg->edate)
2042 } else { #pkgnum tax
2043 next unless $cust_bill_pkg->setup != 0;
2044 $pkg = $cust_bill_pkg->desc;
2045 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2046 ( $sdate, $edate ) = ( '', '' );
2052 ( map { '' } (1..11) ),
2053 ($pkg, $setup, $recur, $sdate, $edate)
2054 ) or die "can't create csv";
2056 $detail .= $csv->string. "\n";
2062 ( $header, $detail );
2068 Pays this invoice with a compliemntary payment. If there is an error,
2069 returns the error, otherwise returns false.
2075 my $cust_pay = new FS::cust_pay ( {
2076 'invnum' => $self->invnum,
2077 'paid' => $self->owed,
2080 'payinfo' => $self->cust_main->payinfo,
2088 Attempts to pay this invoice with a credit card payment via a
2089 Business::OnlinePayment realtime gateway. See
2090 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2091 for supported processors.
2097 $self->realtime_bop( 'CC', @_ );
2102 Attempts to pay this invoice with an electronic check (ACH) payment via a
2103 Business::OnlinePayment realtime gateway. See
2104 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2105 for supported processors.
2111 $self->realtime_bop( 'ECHECK', @_ );
2116 Attempts to pay this invoice with phone bill (LEC) payment via a
2117 Business::OnlinePayment realtime gateway. See
2118 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2119 for supported processors.
2125 $self->realtime_bop( 'LEC', @_ );
2129 my( $self, $method ) = (shift,shift);
2130 my $conf = $self->conf;
2133 my $cust_main = $self->cust_main;
2134 my $balance = $cust_main->balance;
2135 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2136 $amount = sprintf("%.2f", $amount);
2137 return "not run (balance $balance)" unless $amount > 0;
2139 my $description = 'Internet Services';
2140 if ( $conf->exists('business-onlinepayment-description') ) {
2141 my $dtempl = $conf->config('business-onlinepayment-description');
2143 my $agent_obj = $cust_main->agent
2144 or die "can't retreive agent for $cust_main (agentnum ".
2145 $cust_main->agentnum. ")";
2146 my $agent = $agent_obj->agent;
2147 my $pkgs = join(', ',
2148 map { $_->part_pkg->pkg }
2149 grep { $_->pkgnum } $self->cust_bill_pkg
2151 $description = eval qq("$dtempl");
2154 $cust_main->realtime_bop($method, $amount,
2155 'description' => $description,
2156 'invnum' => $self->invnum,
2157 #this didn't do what we want, it just calls apply_payments_and_credits
2159 'apply_to_invoice' => 1,
2162 #this changes application behavior: auto payments
2163 #triggered against a specific invoice are now applied
2164 #to that invoice instead of oldest open.
2170 =item batch_card OPTION => VALUE...
2172 Adds a payment for this invoice to the pending credit card batch (see
2173 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2174 runs the payment using a realtime gateway.
2179 my ($self, %options) = @_;
2180 my $cust_main = $self->cust_main;
2182 $options{invnum} = $self->invnum;
2184 $cust_main->batch_card(%options);
2187 sub _agent_template {
2189 $self->cust_main->agent_template;
2192 sub _agent_invoice_from {
2194 $self->cust_main->agent_invoice_from;
2197 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2199 Returns an text invoice, as a list of lines.
2201 Options can be passed as a hashref (recommended) or as a list of time, template
2202 and then any key/value pairs for any other options.
2204 I<time>, if specified, is used to control the printing of overdue messages. The
2205 default is now. It isn't the date of the invoice; that's the `_date' field.
2206 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2207 L<Time::Local> and L<Date::Parse> for conversion functions.
2209 I<template>, if specified, is the name of a suffix for alternate invoices.
2211 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2217 my( $today, $template, %opt );
2219 %opt = %{ shift() };
2220 $today = delete($opt{'time'}) || '';
2221 $template = delete($opt{template}) || '';
2223 ( $today, $template, %opt ) = @_;
2226 my %params = ( 'format' => 'template' );
2227 $params{'time'} = $today if $today;
2228 $params{'template'} = $template if $template;
2229 $params{$_} = $opt{$_}
2230 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2232 $self->print_generic( %params );
2235 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2237 Internal method - returns a filename of a filled-in LaTeX template for this
2238 invoice (Note: add ".tex" to get the actual filename), and a filename of
2239 an associated logo (with the .eps extension included).
2241 See print_ps and print_pdf for methods that return PostScript and PDF output.
2243 Options can be passed as a hashref (recommended) or as a list of time, template
2244 and then any key/value pairs for any other options.
2246 I<time>, if specified, is used to control the printing of overdue messages. The
2247 default is now. It isn't the date of the invoice; that's the `_date' field.
2248 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2249 L<Time::Local> and L<Date::Parse> for conversion functions.
2251 I<template>, if specified, is the name of a suffix for alternate invoices.
2253 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2259 my $conf = $self->conf;
2260 my( $today, $template, %opt );
2262 %opt = %{ shift() };
2263 $today = delete($opt{'time'}) || '';
2264 $template = delete($opt{template}) || '';
2266 ( $today, $template, %opt ) = @_;
2269 my %params = ( 'format' => 'latex' );
2270 $params{'time'} = $today if $today;
2271 $params{'template'} = $template if $template;
2272 $params{$_} = $opt{$_}
2273 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2275 $template ||= $self->_agent_template;
2277 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2278 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2282 ) or die "can't open temp file: $!\n";
2284 my $agentnum = $self->cust_main->agentnum;
2286 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2287 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2288 or die "can't write temp file: $!\n";
2290 print $lh $conf->config_binary('logo.eps', $agentnum)
2291 or die "can't write temp file: $!\n";
2294 $params{'logo_file'} = $lh->filename;
2296 if($conf->exists('invoice-barcode')){
2297 my $png_file = $self->invoice_barcode($dir);
2298 my $eps_file = $png_file;
2299 $eps_file =~ s/\.png$/.eps/g;
2300 $png_file =~ /(barcode.*png)/;
2302 $eps_file =~ /(barcode.*eps)/;
2305 my $curr_dir = cwd();
2307 # after painfuly long experimentation, it was determined that sam2p won't
2308 # accept : and other chars in the path, no matter how hard I tried to
2309 # escape them, hence the chdir (and chdir back, just to be safe)
2310 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2311 or die "sam2p failed: $!\n";
2315 $params{'barcode_file'} = $eps_file;
2318 my @filled_in = $self->print_generic( %params );
2320 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2324 ) or die "can't open temp file: $!\n";
2325 binmode($fh, ':utf8'); # language support
2326 print $fh join('', @filled_in );
2329 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2330 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2334 =item invoice_barcode DIR_OR_FALSE
2336 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2337 it is taken as the temp directory where the PNG file will be generated and the
2338 PNG file name is returned. Otherwise, the PNG image itself is returned.
2342 sub invoice_barcode {
2343 my ($self, $dir) = (shift,shift);
2345 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2346 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2347 my $gd = $gdbar->plot(Height => 30);
2350 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2354 ) or die "can't open temp file: $!\n";
2355 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2356 my $png_file = $bh->filename;
2363 =item print_generic OPTION => VALUE ...
2365 Internal method - returns a filled-in template for this invoice as a scalar.
2367 See print_ps and print_pdf for methods that return PostScript and PDF output.
2369 Non optional options include
2370 format - latex, html, template
2372 Optional options include
2374 template - a value used as a suffix for a configuration template
2376 time - a value used to control the printing of overdue messages. The
2377 default is now. It isn't the date of the invoice; that's the `_date' field.
2378 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2379 L<Time::Local> and L<Date::Parse> for conversion functions.
2383 unsquelch_cdr - overrides any per customer cdr squelching when true
2385 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2387 locale - override customer's locale
2391 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2392 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2393 # yes: fixed width/plain text printing will be borked
2395 my( $self, %params ) = @_;
2396 my $conf = $self->conf;
2397 my $today = $params{today} ? $params{today} : time;
2398 warn "$me print_generic called on $self with suffix $params{template}\n"
2401 my $format = $params{format};
2402 die "Unknown format: $format"
2403 unless $format =~ /^(latex|html|template)$/;
2405 my $cust_main = $self->cust_main;
2406 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2407 unless $cust_main->payname
2408 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2410 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2411 'html' => [ '<%=', '%>' ],
2412 'template' => [ '{', '}' ],
2415 warn "$me print_generic creating template\n"
2418 #create the template
2419 my $template = $params{template} ? $params{template} : $self->_agent_template;
2420 my $templatefile = "invoice_$format";
2421 $templatefile .= "_$template"
2422 if length($template) && $conf->exists($templatefile."_$template");
2423 my @invoice_template = map "$_\n", $conf->config($templatefile)
2424 or die "cannot load config data $templatefile";
2427 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2428 #change this to a die when the old code is removed
2429 warn "old-style invoice template $templatefile; ".
2430 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2431 $old_latex = 'true';
2432 @invoice_template = _translate_old_latex_format(@invoice_template);
2435 warn "$me print_generic creating T:T object\n"
2438 my $text_template = new Text::Template(
2440 SOURCE => \@invoice_template,
2441 DELIMITERS => $delimiters{$format},
2444 warn "$me print_generic compiling T:T object\n"
2447 $text_template->compile()
2448 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2451 # additional substitution could possibly cause breakage in existing templates
2452 my %convert_maps = (
2454 'notes' => sub { map "$_", @_ },
2455 'footer' => sub { map "$_", @_ },
2456 'smallfooter' => sub { map "$_", @_ },
2457 'returnaddress' => sub { map "$_", @_ },
2458 'coupon' => sub { map "$_", @_ },
2459 'summary' => sub { map "$_", @_ },
2465 s/%%(.*)$/<!-- $1 -->/g;
2466 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2467 s/\\begin\{enumerate\}/<ol>/g;
2469 s/\\end\{enumerate\}/<\/ol>/g;
2470 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2479 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2481 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2486 s/\\\\\*?\s*$/<BR>/;
2487 s/\\hyphenation\{[\w\s\-]+}//;
2492 'coupon' => sub { "" },
2493 'summary' => sub { "" },
2500 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2501 s/\\begin\{enumerate\}//g;
2503 s/\\end\{enumerate\}//g;
2504 s/\\textbf\{(.*)\}/$1/g;
2511 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2513 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2518 s/\\\\\*?\s*$/\n/; # dubious
2519 s/\\hyphenation\{[\w\s\-]+}//;
2523 'coupon' => sub { "" },
2524 'summary' => sub { "" },
2529 # hashes for differing output formats
2530 my %nbsps = ( 'latex' => '~',
2531 'html' => '', # '&nbps;' would be nice
2532 'template' => '', # not used
2534 my $nbsp = $nbsps{$format};
2536 my %escape_functions = ( 'latex' => \&_latex_escape,
2537 'html' => \&_html_escape_nbsp,#\&encode_entities,
2538 'template' => sub { shift },
2540 my $escape_function = $escape_functions{$format};
2541 my $escape_function_nonbsp = ($format eq 'html')
2542 ? \&_html_escape : $escape_function;
2544 my %date_formats = ( 'latex' => $date_format_long,
2545 'html' => $date_format_long,
2548 $date_formats{'html'} =~ s/ / /g;
2550 my $date_format = $date_formats{$format};
2552 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2554 'html' => sub { return '<b>'. shift(). '</b>'
2556 'template' => sub { shift },
2558 my $embolden_function = $embolden_functions{$format};
2560 my %newline_tokens = ( 'latex' => '\\\\',
2564 my $newline_token = $newline_tokens{$format};
2566 warn "$me generating template variables\n"
2569 # generate template variables
2572 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2576 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2582 $returnaddress = join("\n",
2583 $conf->config_orbase("invoice_${format}returnaddress", $template)
2586 } elsif ( grep /\S/,
2587 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2589 my $convert_map = $convert_maps{$format}{'returnaddress'};
2592 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2597 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2599 my $convert_map = $convert_maps{$format}{'returnaddress'};
2600 $returnaddress = join( "\n", &$convert_map(
2601 map { s/( {2,})/'~' x length($1)/eg;
2605 ( $conf->config('company_name', $self->cust_main->agentnum),
2606 $conf->config('company_address', $self->cust_main->agentnum),
2613 my $warning = "Couldn't find a return address; ".
2614 "do you need to set the company_address configuration value?";
2616 $returnaddress = $nbsp;
2617 #$returnaddress = $warning;
2621 warn "$me generating invoice data\n"
2624 my $agentnum = $self->cust_main->agentnum;
2626 my %invoice_data = (
2629 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2630 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2631 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2632 'returnaddress' => $returnaddress,
2633 'agent' => &$escape_function($cust_main->agent->agent),
2636 'invnum' => $self->invnum,
2637 'date' => time2str($date_format, $self->_date),
2638 'today' => time2str($date_format_long, $today),
2639 'terms' => $self->terms,
2640 'template' => $template, #params{'template'},
2641 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2642 'current_charges' => sprintf("%.2f", $self->charged),
2643 'duedate' => $self->due_date2str($rdate_format), #date_format?
2646 'custnum' => $cust_main->display_custnum,
2647 'agent_custid' => &$escape_function($cust_main->agent_custid),
2648 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2649 payname company address1 address2 city state zip fax
2653 'ship_enable' => $conf->exists('invoice-ship_address'),
2654 'unitprices' => $conf->exists('invoice-unitprice'),
2655 'smallernotes' => $conf->exists('invoice-smallernotes'),
2656 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2657 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2659 #layout info -- would be fancy to calc some of this and bury the template
2661 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2662 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2663 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2664 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2665 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2666 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2667 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2668 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2669 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2670 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2672 # better hang on to conf_dir for a while (for old templates)
2673 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2675 #these are only used when doing paged plaintext
2682 my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
2683 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2684 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2685 # eval to avoid death for unimplemented languages
2686 my $dh = eval { Date::Language->new($info{'name'}) } ||
2687 Date::Language->new(); # fall back to English
2688 # prototype here to silence warnings
2689 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2690 # eventually use this date handle everywhere in here, too
2692 my $min_sdate = 999999999999;
2694 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2695 next unless $cust_bill_pkg->pkgnum > 0;
2696 $min_sdate = $cust_bill_pkg->sdate
2697 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2698 $max_edate = $cust_bill_pkg->edate
2699 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2702 $invoice_data{'bill_period'} = '';
2703 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2704 . " to " . time2str('%e %h', $max_edate)
2705 if ($max_edate != 0 && $min_sdate != 999999999999);
2707 $invoice_data{finance_section} = '';
2708 if ( $conf->config('finance_pkgclass') ) {
2710 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2711 $invoice_data{finance_section} = $pkg_class->categoryname;
2713 $invoice_data{finance_amount} = '0.00';
2714 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2716 my $countrydefault = $conf->config('countrydefault') || 'US';
2717 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2718 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2719 my $method = $prefix.$_;
2720 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2722 $invoice_data{'ship_country'} = ''
2723 if ( $invoice_data{'ship_country'} eq $countrydefault );
2725 $invoice_data{'cid'} = $params{'cid'}
2728 if ( $cust_main->country eq $countrydefault ) {
2729 $invoice_data{'country'} = '';
2731 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2735 $invoice_data{'address'} = \@address;
2737 $cust_main->payname.
2738 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2739 ? " (P.O. #". $cust_main->payinfo. ")"
2743 push @address, $cust_main->company
2744 if $cust_main->company;
2745 push @address, $cust_main->address1;
2746 push @address, $cust_main->address2
2747 if $cust_main->address2;
2749 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2750 push @address, $invoice_data{'country'}
2751 if $invoice_data{'country'};
2753 while (scalar(@address) < 5);
2755 $invoice_data{'logo_file'} = $params{'logo_file'}
2756 if $params{'logo_file'};
2757 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2758 if $params{'barcode_file'};
2759 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2760 if $params{'barcode_img'};
2761 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2762 if $params{'barcode_cid'};
2764 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2765 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2766 #my $balance_due = $self->owed + $pr_total - $cr_total;
2767 my $balance_due = $self->owed + $pr_total;
2769 # the customer's current balance as shown on the invoice before this one
2770 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2772 # the change in balance from that invoice to this one
2773 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2775 # the sum of amount owed on all previous invoices
2776 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2778 # the sum of amount owed on all invoices
2779 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2781 # info from customer's last invoice before this one, for some
2783 $invoice_data{'last_bill'} = {};
2784 my $last_bill = $pr_cust_bill[-1];
2786 $invoice_data{'last_bill'} = {
2787 '_date' => $last_bill->_date, #unformatted
2788 # all we need for now
2792 my $summarypage = '';
2793 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2796 $invoice_data{'summarypage'} = $summarypage;
2798 warn "$me substituting variables in notes, footer, smallfooter\n"
2801 my @include = (qw( notes footer smallfooter ));
2802 push @include, 'coupon' unless $params{'no_coupon'};
2803 foreach my $include (@include) {
2805 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2808 if ( $conf->exists($inc_file, $agentnum)
2809 && length( $conf->config($inc_file, $agentnum) ) ) {
2811 @inc_src = $conf->config($inc_file, $agentnum);
2815 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2817 my $convert_map = $convert_maps{$format}{$include};
2819 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2820 s/--\@\]/$delimiters{$format}[1]/g;
2823 &$convert_map( $conf->config($inc_file, $agentnum) );
2827 my $inc_tt = new Text::Template (
2829 SOURCE => [ map "$_\n", @inc_src ],
2830 DELIMITERS => $delimiters{$format},
2831 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2833 unless ( $inc_tt->compile() ) {
2834 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2835 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2839 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2841 $invoice_data{$include} =~ s/\n+$//
2842 if ($format eq 'latex');
2845 # let invoices use either of these as needed
2846 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2847 ? $cust_main->payinfo : '';
2848 $invoice_data{'po_line'} =
2849 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2850 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2853 my %money_chars = ( 'latex' => '',
2854 'html' => $conf->config('money_char') || '$',
2857 my $money_char = $money_chars{$format};
2859 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2860 'html' => $conf->config('money_char') || '$',
2863 my $other_money_char = $other_money_chars{$format};
2864 $invoice_data{'dollar'} = $other_money_char;
2866 my @detail_items = ();
2867 my @total_items = ();
2871 $invoice_data{'detail_items'} = \@detail_items;
2872 $invoice_data{'total_items'} = \@total_items;
2873 $invoice_data{'buf'} = \@buf;
2874 $invoice_data{'sections'} = \@sections;
2876 warn "$me generating sections\n"
2879 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2880 'subtotal' => $other_money_char.
2881 sprintf('%.2f', $pr_total),
2882 'summarized' => '', #why? $summarypage ? 'Y' : '',
2884 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2885 join(' / ', map { $cust_main->balance_date_range(@$_) }
2886 $self->_prior_month30s
2888 if $conf->exists('invoice_include_aging');
2891 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2892 'subtotal' => $taxtotal, # adjusted below
2894 my $tax_weight = _pkg_category($tax_section->{description})
2895 ? _pkg_category($tax_section->{description})->weight
2897 $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
2898 $tax_section->{'sort_weight'} = $tax_weight;
2901 my $adjusttotal = 0;
2902 my $adjust_section = { 'description' =>
2903 $self->mt('Credits, Payments, and Adjustments'),
2904 'subtotal' => 0, # adjusted below
2906 my $adjust_weight = _pkg_category($adjust_section->{description})
2907 ? _pkg_category($adjust_section->{description})->weight
2909 $adjust_section->{'summarized'} = ''; #why? $summarypage && !$adjust_weight ? 'Y' : '';
2910 $adjust_section->{'sort_weight'} = $adjust_weight;
2912 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2913 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2914 $invoice_data{'multisection'} = $multisection;
2915 my $late_sections = [];
2916 my $extra_sections = [];
2917 my $extra_lines = ();
2918 if ( $multisection ) {
2919 ($extra_sections, $extra_lines) =
2920 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2921 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2923 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2925 push @detail_items, @$extra_lines if $extra_lines;
2927 $self->_items_sections( $late_sections, # this could stand a refactor
2929 $escape_function_nonbsp,
2933 if ($conf->exists('svc_phone_sections')) {
2934 my ($phone_sections, $phone_lines) =
2935 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2936 push @{$late_sections}, @$phone_sections;
2937 push @detail_items, @$phone_lines;
2939 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2940 my ($accountcode_section, $accountcode_lines) =
2941 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2942 if ( scalar(@$accountcode_lines) ) {
2943 push @{$late_sections}, $accountcode_section;
2944 push @detail_items, @$accountcode_lines;
2947 } else {# not multisection
2948 # make a default section
2949 push @sections, { 'description' => '', 'subtotal' => '',
2950 'no_subtotal' => 1 };
2951 # and calculate the finance charge total, since it won't get done otherwise.
2952 # XXX possibly other totals?
2953 # XXX possibly finance_pkgclass should not be used in this manner?
2954 if ( $conf->exists('finance_pkgclass') ) {
2955 my @finance_charges;
2956 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2957 if ( grep { $_->section eq $invoice_data{finance_section} }
2958 $cust_bill_pkg->cust_bill_pkg_display ) {
2959 # I think these are always setup fees, but just to be sure...
2960 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2963 $invoice_data{finance_amount} =
2964 sprintf('%.2f', sum( @finance_charges ) || 0);
2968 unless ( $conf->exists('disable_previous_balance')
2969 || $conf->exists('previous_balance-summary_only')
2973 warn "$me adding previous balances\n"
2976 foreach my $line_item ( $self->_items_previous ) {
2979 ext_description => [],
2981 $detail->{'ref'} = $line_item->{'pkgnum'};
2982 $detail->{'quantity'} = 1;
2983 $detail->{'section'} = $previous_section;
2984 $detail->{'description'} = &$escape_function($line_item->{'description'});
2985 if ( exists $line_item->{'ext_description'} ) {
2986 @{$detail->{'ext_description'}} = map {
2987 &$escape_function($_);
2988 } @{$line_item->{'ext_description'}};
2990 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2991 $line_item->{'amount'};
2992 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2994 push @detail_items, $detail;
2995 push @buf, [ $detail->{'description'},
2996 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3002 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
3003 push @buf, ['','-----------'];
3004 push @buf, [ $self->mt('Total Previous Balance'),
3005 $money_char. sprintf("%10.2f", $pr_total) ];
3009 if ( $conf->exists('svc_phone-did-summary') ) {
3010 warn "$me adding DID summary\n"
3013 my ($didsummary,$minutes) = $self->_did_summary;
3014 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
3016 { 'description' => $didsummary_desc,
3017 'ext_description' => [ $didsummary, $minutes ],
3021 foreach my $section (@sections, @$late_sections) {
3023 warn "$me adding section \n". Dumper($section)
3026 # begin some normalization
3027 $section->{'subtotal'} = $section->{'amount'}
3029 && !exists($section->{subtotal})
3030 && exists($section->{amount});
3032 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3033 if ( $invoice_data{finance_section} &&
3034 $section->{'description'} eq $invoice_data{finance_section} );
3036 $section->{'subtotal'} = $other_money_char.
3037 sprintf('%.2f', $section->{'subtotal'})
3040 # continue some normalization
3041 $section->{'amount'} = $section->{'subtotal'}
3045 if ( $section->{'description'} ) {
3046 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3051 warn "$me setting options\n"
3054 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3056 $options{'section'} = $section if $multisection;
3057 $options{'format'} = $format;
3058 $options{'escape_function'} = $escape_function;
3059 $options{'no_usage'} = 1 unless $unsquelched;
3060 $options{'unsquelched'} = $unsquelched;
3061 $options{'summary_page'} = $summarypage;
3062 $options{'skip_usage'} =
3063 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3064 $options{'multilocation'} = $multilocation;
3065 $options{'multisection'} = $multisection;
3067 warn "$me searching for line items\n"
3070 foreach my $line_item ( $self->_items_pkg(%options) ) {
3072 warn "$me adding line item $line_item\n"
3076 ext_description => [],
3078 $detail->{'ref'} = $line_item->{'pkgnum'};
3079 $detail->{'quantity'} = $line_item->{'quantity'};
3080 $detail->{'section'} = $section;
3081 $detail->{'description'} = &$escape_function($line_item->{'description'});
3082 if ( exists $line_item->{'ext_description'} ) {
3083 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3085 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3086 $line_item->{'amount'};
3087 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3088 $line_item->{'unit_amount'};
3089 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3091 $detail->{'sdate'} = $line_item->{'sdate'};
3092 $detail->{'edate'} = $line_item->{'edate'};
3093 $detail->{'seconds'} = $line_item->{'seconds'};
3095 push @detail_items, $detail;
3096 push @buf, ( [ $detail->{'description'},
3097 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3099 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3103 if ( $section->{'description'} ) {
3104 push @buf, ( ['','-----------'],
3105 [ $section->{'description'}. ' sub-total',
3106 $section->{'subtotal'} # already formatted this
3115 $invoice_data{current_less_finance} =
3116 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3118 if ( $multisection && !$conf->exists('disable_previous_balance')
3119 || $conf->exists('previous_balance-summary_only') )
3121 unshift @sections, $previous_section if $pr_total;
3124 warn "$me adding taxes\n"
3127 foreach my $tax ( $self->_items_tax ) {
3129 $taxtotal += $tax->{'amount'};
3131 my $description = &$escape_function( $tax->{'description'} );
3132 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3134 if ( $multisection ) {
3136 my $money = $old_latex ? '' : $money_char;
3137 push @detail_items, {
3138 ext_description => [],
3141 description => $description,
3142 amount => $money. $amount,
3144 section => $tax_section,
3149 push @total_items, {
3150 'total_item' => $description,
3151 'total_amount' => $other_money_char. $amount,
3156 push @buf,[ $description,
3157 $money_char. $amount,
3164 $total->{'total_item'} = $self->mt('Sub-total');
3165 $total->{'total_amount'} =
3166 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3168 if ( $multisection ) {
3169 $tax_section->{'subtotal'} = $other_money_char.
3170 sprintf('%.2f', $taxtotal);
3171 $tax_section->{'pretotal'} = 'New charges sub-total '.
3172 $total->{'total_amount'};
3173 push @sections, $tax_section if $taxtotal;
3175 unshift @total_items, $total;
3178 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3180 push @buf,['','-----------'];
3181 push @buf,[$self->mt(
3182 $conf->exists('disable_previous_balance')
3184 : 'Total New Charges'
3186 $money_char. sprintf("%10.2f",$self->charged) ];
3192 $item = $conf->config('previous_balance-exclude_from_total')
3193 || 'Total New Charges'
3194 if $conf->exists('previous_balance-exclude_from_total');
3195 my $amount = $self->charged +
3196 ( $conf->exists('disable_previous_balance') ||
3197 $conf->exists('previous_balance-exclude_from_total')
3201 $total->{'total_item'} = &$embolden_function($self->mt($item));
3202 $total->{'total_amount'} =
3203 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3204 if ( $multisection ) {
3205 if ( $adjust_section->{'sort_weight'} ) {
3206 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3207 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3209 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3210 $other_money_char. sprintf('%.2f', $self->charged );
3213 push @total_items, $total;
3215 push @buf,['','-----------'];
3218 sprintf( '%10.2f', $amount )
3223 unless ( $conf->exists('disable_previous_balance') ) {
3224 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3227 my $credittotal = 0;
3228 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3231 $total->{'total_item'} = &$escape_function($credit->{'description'});
3232 $credittotal += $credit->{'amount'};
3233 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3234 $adjusttotal += $credit->{'amount'};
3235 if ( $multisection ) {
3236 my $money = $old_latex ? '' : $money_char;
3237 push @detail_items, {
3238 ext_description => [],
3241 description => &$escape_function($credit->{'description'}),
3242 amount => $money. $credit->{'amount'},
3244 section => $adjust_section,
3247 push @total_items, $total;
3251 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3254 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3255 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3259 my $paymenttotal = 0;
3260 foreach my $payment ( $self->_items_payments ) {
3262 $total->{'total_item'} = &$escape_function($payment->{'description'});
3263 $paymenttotal += $payment->{'amount'};
3264 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3265 $adjusttotal += $payment->{'amount'};
3266 if ( $multisection ) {
3267 my $money = $old_latex ? '' : $money_char;
3268 push @detail_items, {
3269 ext_description => [],
3272 description => &$escape_function($payment->{'description'}),
3273 amount => $money. $payment->{'amount'},
3275 section => $adjust_section,
3278 push @total_items, $total;
3280 push @buf, [ $payment->{'description'},
3281 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3284 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3286 if ( $multisection ) {
3287 $adjust_section->{'subtotal'} = $other_money_char.
3288 sprintf('%.2f', $adjusttotal);
3289 push @sections, $adjust_section
3290 unless $adjust_section->{sort_weight};
3293 # create Balance Due message
3296 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3297 $total->{'total_amount'} =
3298 &$embolden_function(
3299 $other_money_char. sprintf('%.2f', $summarypage
3301 $self->billing_balance
3302 : $self->owed + $pr_total
3305 if ( $multisection && !$adjust_section->{sort_weight} ) {
3306 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3307 $total->{'total_amount'};
3309 push @total_items, $total;
3311 push @buf,['','-----------'];
3312 push @buf,[$self->balance_due_msg, $money_char.
3313 sprintf("%10.2f", $balance_due ) ];
3316 if ( $conf->exists('previous_balance-show_credit')
3317 and $cust_main->balance < 0 ) {
3318 my $credit_total = {
3319 'total_item' => &$embolden_function($self->credit_balance_msg),
3320 'total_amount' => &$embolden_function(
3321 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3324 if ( $multisection ) {
3325 $adjust_section->{'posttotal'} .= $newline_token .
3326 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3329 push @total_items, $credit_total;
3331 push @buf,['','-----------'];
3332 push @buf,[$self->credit_balance_msg, $money_char.
3333 sprintf("%10.2f", -$cust_main->balance ) ];
3337 if ( $multisection ) {
3338 if ($conf->exists('svc_phone_sections')) {
3340 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3341 $total->{'total_amount'} =
3342 &$embolden_function(
3343 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3345 my $last_section = pop @sections;
3346 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3347 $total->{'total_amount'};
3348 push @sections, $last_section;
3350 push @sections, @$late_sections
3354 # make a discounts-available section, even without multisection
3355 if ( $conf->exists('discount-show_available')
3356 and my @discounts_avail = $self->_items_discounts_avail ) {
3357 my $discount_section = {
3358 'description' => $self->mt('Discounts Available'),
3363 push @sections, $discount_section;
3364 push @detail_items, map { +{
3365 'ref' => '', #should this be something else?
3366 'section' => $discount_section,
3367 'description' => &$escape_function( $_->{description} ),
3368 'amount' => $money_char . &$escape_function( $_->{amount} ),
3369 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3370 } } @discounts_avail;
3373 # All sections and items are built; now fill in templates.
3374 my @includelist = ();
3375 push @includelist, 'summary' if $summarypage;
3376 foreach my $include ( @includelist ) {
3378 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3381 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3383 @inc_src = $conf->config($inc_file, $agentnum);
3387 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3389 my $convert_map = $convert_maps{$format}{$include};
3391 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3392 s/--\@\]/$delimiters{$format}[1]/g;
3395 &$convert_map( $conf->config($inc_file, $agentnum) );
3399 my $inc_tt = new Text::Template (
3401 SOURCE => [ map "$_\n", @inc_src ],
3402 DELIMITERS => $delimiters{$format},
3403 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3405 unless ( $inc_tt->compile() ) {
3406 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3407 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3411 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3413 $invoice_data{$include} =~ s/\n+$//
3414 if ($format eq 'latex');
3419 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3420 /invoice_lines\((\d*)\)/;
3421 $invoice_lines += $1 || scalar(@buf);
3424 die "no invoice_lines() functions in template?"
3425 if ( $format eq 'template' && !$wasfunc );
3427 if ($format eq 'template') {
3429 if ( $invoice_lines ) {
3430 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3431 $invoice_data{'total_pages'}++
3432 if scalar(@buf) % $invoice_lines;
3435 #setup subroutine for the template
3436 $invoice_data{invoice_lines} = sub {
3437 my $lines = shift || scalar(@buf);
3449 push @collect, split("\n",
3450 $text_template->fill_in( HASH => \%invoice_data )
3452 $invoice_data{'page'}++;
3454 map "$_\n", @collect;
3456 # this is where we actually create the invoice
3457 warn "filling in template for invoice ". $self->invnum. "\n"
3459 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3462 $text_template->fill_in(HASH => \%invoice_data);
3466 # helper routine for generating date ranges
3467 sub _prior_month30s {
3470 [ 1, 2592000 ], # 0-30 days ago
3471 [ 2592000, 5184000 ], # 30-60 days ago
3472 [ 5184000, 7776000 ], # 60-90 days ago
3473 [ 7776000, 0 ], # 90+ days ago
3476 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3477 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3482 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3484 Returns an postscript invoice, as a scalar.
3486 Options can be passed as a hashref (recommended) or as a list of time, template
3487 and then any key/value pairs for any other options.
3489 I<time> an optional value used to control the printing of overdue messages. The
3490 default is now. It isn't the date of the invoice; that's the `_date' field.
3491 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3492 L<Time::Local> and L<Date::Parse> for conversion functions.
3494 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3501 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3502 my $ps = generate_ps($file);
3504 unlink($barcodefile) if $barcodefile;
3509 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3511 Returns an PDF invoice, as a scalar.
3513 Options can be passed as a hashref (recommended) or as a list of time, template
3514 and then any key/value pairs for any other options.
3516 I<time> an optional value used to control the printing of overdue messages. The
3517 default is now. It isn't the date of the invoice; that's the `_date' field.
3518 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3519 L<Time::Local> and L<Date::Parse> for conversion functions.
3521 I<template>, if specified, is the name of a suffix for alternate invoices.
3523 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3530 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3531 my $pdf = generate_pdf($file);
3533 unlink($barcodefile) if $barcodefile;
3538 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3540 Returns an HTML invoice, as a scalar.
3542 I<time> an optional value used to control the printing of overdue messages. The
3543 default is now. It isn't the date of the invoice; that's the `_date' field.
3544 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3545 L<Time::Local> and L<Date::Parse> for conversion functions.
3547 I<template>, if specified, is the name of a suffix for alternate invoices.
3549 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3551 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3552 when emailing the invoice as part of a multipart/related MIME email.
3560 %params = %{ shift() };
3562 $params{'time'} = shift;
3563 $params{'template'} = shift;
3564 $params{'cid'} = shift;
3567 $params{'format'} = 'html';
3569 $self->print_generic( %params );
3572 # quick subroutine for print_latex
3574 # There are ten characters that LaTeX treats as special characters, which
3575 # means that they do not simply typeset themselves:
3576 # # $ % & ~ _ ^ \ { }
3578 # TeX ignores blanks following an escaped character; if you want a blank (as
3579 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3583 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3584 $value =~ s/([<>])/\$$1\$/g;
3590 encode_entities($value);
3594 sub _html_escape_nbsp {
3595 my $value = _html_escape(shift);
3596 $value =~ s/ +/ /g;
3600 #utility methods for print_*
3602 sub _translate_old_latex_format {
3603 warn "_translate_old_latex_format called\n"
3610 if ( $line =~ /^%%Detail\s*$/ ) {
3612 push @template, q![@--!,
3613 q! foreach my $_tr_line (@detail_items) {!,
3614 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3615 q! $_tr_line->{'description'} .= !,
3616 q! "\\tabularnewline\n~~".!,
3617 q! join( "\\tabularnewline\n~~",!,
3618 q! @{$_tr_line->{'ext_description'}}!,
3622 while ( ( my $line_item_line = shift )
3623 !~ /^%%EndDetail\s*$/ ) {
3624 $line_item_line =~ s/'/\\'/g; # nice LTS
3625 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3626 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3627 push @template, " \$OUT .= '$line_item_line';";
3630 push @template, '}',
3633 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3635 push @template, '[@--',
3636 ' foreach my $_tr_line (@total_items) {';
3638 while ( ( my $total_item_line = shift )
3639 !~ /^%%EndTotalDetails\s*$/ ) {
3640 $total_item_line =~ s/'/\\'/g; # nice LTS
3641 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3642 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3643 push @template, " \$OUT .= '$total_item_line';";
3646 push @template, '}',
3650 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3651 push @template, $line;
3657 warn "$_\n" foreach @template;
3665 my $conf = $self->conf;
3667 #check for an invoice-specific override
3668 return $self->invoice_terms if $self->invoice_terms;
3670 #check for a customer- specific override
3671 my $cust_main = $self->cust_main;
3672 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3674 #use configured default
3675 $conf->config('invoice_default_terms') || '';
3681 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3682 $duedate = $self->_date() + ( $1 * 86400 );
3689 $self->due_date ? time2str(shift, $self->due_date) : '';
3692 sub balance_due_msg {
3694 my $msg = $self->mt('Balance Due');
3695 return $msg unless $self->terms;
3696 if ( $self->due_date ) {
3697 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3698 $self->due_date2str($date_format);
3699 } elsif ( $self->terms ) {
3700 $msg .= ' - '. $self->terms;
3705 sub balance_due_date {
3707 my $conf = $self->conf;
3709 if ( $conf->exists('invoice_default_terms')
3710 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3711 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3716 sub credit_balance_msg {
3718 $self->mt('Credit Balance Remaining')
3721 =item invnum_date_pretty
3723 Returns a string with the invoice number and date, for example:
3724 "Invoice #54 (3/20/2008)"
3728 sub invnum_date_pretty {
3730 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3735 Returns a string with the date, for example: "3/20/2008"
3741 time2str($date_format, $self->_date);
3744 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3746 Generate section information for all items appearing on this invoice.
3747 This will only be called for multi-section invoices.
3749 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3750 related display records (L<FS::cust_bill_pkg_display>) and organize
3751 them into two groups ("early" and "late" according to whether they come
3752 before or after the total), then into sections. A subtotal is calculated
3755 Section descriptions are returned in sort weight order. Each consists
3756 of a hash containing:
3758 description: the package category name, escaped
3759 subtotal: the total charges in that section
3760 tax_section: a flag indicating that the section contains only tax charges
3761 summarized: same as tax_section, for some reason
3762 sort_weight: the package category's sort weight
3764 If 'condense' is set on the display record, it also contains everything
3765 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3766 coderefs to generate parts of the invoice. This is not advised.
3770 LATE: an arrayref to push the "late" section hashes onto. The "early"
3771 group is simply returned from the method.
3773 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3774 Turning this on has the following effects:
3775 - Ignores display items with the 'summary' flag.
3776 - Combines all items into the "early" group.
3777 - Creates sections for all non-disabled package categories, even if they
3778 have no charges on this invoice, as well as a section with no name.
3780 ESCAPE: an escape function to use for section titles.
3782 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3783 sorted list. If there are any of these, section subtotals exclude
3786 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3787 passed through to C<_condense_section()>.
3791 use vars qw(%pkg_category_cache);
3792 sub _items_sections {
3795 my $summarypage = shift;
3797 my $extra_sections = shift;
3801 my %late_subtotal = ();
3804 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3807 my $usage = $cust_bill_pkg->usage;
3809 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3810 next if ( $display->summary && $summarypage );
3812 my $section = $display->section;
3813 my $type = $display->type;
3815 $not_tax{$section} = 1
3816 unless $cust_bill_pkg->pkgnum == 0;
3818 if ( $display->post_total && !$summarypage ) {
3819 if (! $type || $type eq 'S') {
3820 $late_subtotal{$section} += $cust_bill_pkg->setup
3821 if $cust_bill_pkg->setup != 0;
3825 $late_subtotal{$section} += $cust_bill_pkg->recur
3826 if $cust_bill_pkg->recur != 0;
3829 if ($type && $type eq 'R') {
3830 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3831 if $cust_bill_pkg->recur != 0;
3834 if ($type && $type eq 'U') {
3835 $late_subtotal{$section} += $usage
3836 unless scalar(@$extra_sections);
3841 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3843 if (! $type || $type eq 'S') {
3844 $subtotal{$section} += $cust_bill_pkg->setup
3845 if $cust_bill_pkg->setup != 0;
3849 $subtotal{$section} += $cust_bill_pkg->recur
3850 if $cust_bill_pkg->recur != 0;
3853 if ($type && $type eq 'R') {
3854 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3855 if $cust_bill_pkg->recur != 0;
3858 if ($type && $type eq 'U') {
3859 $subtotal{$section} += $usage
3860 unless scalar(@$extra_sections);
3869 %pkg_category_cache = ();
3871 push @$late, map { { 'description' => &{$escape}($_),
3872 'subtotal' => $late_subtotal{$_},
3874 'sort_weight' => ( _pkg_category($_)
3875 ? _pkg_category($_)->weight
3878 ((_pkg_category($_) && _pkg_category($_)->condense)
3879 ? $self->_condense_section($format)
3883 sort _sectionsort keys %late_subtotal;
3886 if ( $summarypage ) {
3887 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3888 map { $_->categoryname } qsearch('pkg_category', {});
3889 push @sections, '' if exists($subtotal{''});
3891 @sections = keys %subtotal;
3894 my @early = map { { 'description' => &{$escape}($_),
3895 'subtotal' => $subtotal{$_},
3896 'summarized' => $not_tax{$_} ? '' : 'Y',
3897 'tax_section' => $not_tax{$_} ? '' : 'Y',
3898 'sort_weight' => ( _pkg_category($_)
3899 ? _pkg_category($_)->weight
3902 ((_pkg_category($_) && _pkg_category($_)->condense)
3903 ? $self->_condense_section($format)
3908 push @early, @$extra_sections if $extra_sections;
3910 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3914 #helper subs for above
3917 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3921 my $categoryname = shift;
3922 $pkg_category_cache{$categoryname} ||=
3923 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3926 my %condensed_format = (
3927 'label' => [ qw( Description Qty Amount ) ],
3929 sub { shift->{description} },
3930 sub { shift->{quantity} },
3931 sub { my($href, %opt) = @_;
3932 ($opt{dollar} || ''). $href->{amount};
3935 'align' => [ qw( l r r ) ],
3936 'span' => [ qw( 5 1 1 ) ], # unitprices?
3937 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3940 sub _condense_section {
3941 my ( $self, $format ) = ( shift, shift );
3943 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3944 qw( description_generator
3947 total_line_generator
3952 sub _condensed_generator_defaults {
3953 my ( $self, $format ) = ( shift, shift );
3954 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3963 sub _condensed_header_generator {
3964 my ( $self, $format ) = ( shift, shift );
3966 my ( $f, $prefix, $suffix, $separator, $column ) =
3967 _condensed_generator_defaults($format);
3969 if ($format eq 'latex') {
3970 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3971 $suffix = "\\\\\n\\hline";
3974 sub { my ($d,$a,$s,$w) = @_;
3975 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3977 } elsif ( $format eq 'html' ) {
3978 $prefix = '<th></th>';
3982 sub { my ($d,$a,$s,$w) = @_;
3983 return qq!<th align="$html_align{$a}">$d</th>!;
3991 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3993 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3996 $prefix. join($separator, @result). $suffix;
4001 sub _condensed_description_generator {
4002 my ( $self, $format ) = ( shift, shift );
4004 my ( $f, $prefix, $suffix, $separator, $column ) =
4005 _condensed_generator_defaults($format);
4007 my $money_char = '$';
4008 if ($format eq 'latex') {
4009 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
4011 $separator = " & \n";
4013 sub { my ($d,$a,$s,$w) = @_;
4014 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
4016 $money_char = '\\dollar';
4017 }elsif ( $format eq 'html' ) {
4018 $prefix = '"><td align="center"></td>';
4022 sub { my ($d,$a,$s,$w) = @_;
4023 return qq!<td align="$html_align{$a}">$d</td>!;
4025 #$money_char = $conf->config('money_char') || '$';
4026 $money_char = ''; # this is madness
4034 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4036 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4038 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4039 map { $f->{$_}->[$i] } qw(align span width)
4043 $prefix. join( $separator, @result ). $suffix;
4048 sub _condensed_total_generator {
4049 my ( $self, $format ) = ( shift, shift );
4051 my ( $f, $prefix, $suffix, $separator, $column ) =
4052 _condensed_generator_defaults($format);
4055 if ($format eq 'latex') {
4058 $separator = " & \n";
4060 sub { my ($d,$a,$s,$w) = @_;
4061 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4063 }elsif ( $format eq 'html' ) {
4067 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4069 sub { my ($d,$a,$s,$w) = @_;
4070 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4079 # my $r = &{$f->{fields}->[$i]}(@args);
4080 # $r .= ' Total' unless $i;
4082 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4084 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4085 map { $f->{$_}->[$i] } qw(align span width)
4089 $prefix. join( $separator, @result ). $suffix;
4094 =item total_line_generator FORMAT
4096 Returns a coderef used for generation of invoice total line items for this
4097 usage_class. FORMAT is either html or latex
4101 # should not be used: will have issues with hash element names (description vs
4102 # total_item and amount vs total_amount -- another array of functions?
4104 sub _condensed_total_line_generator {
4105 my ( $self, $format ) = ( shift, shift );
4107 my ( $f, $prefix, $suffix, $separator, $column ) =
4108 _condensed_generator_defaults($format);
4111 if ($format eq 'latex') {
4114 $separator = " & \n";
4116 sub { my ($d,$a,$s,$w) = @_;
4117 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4119 }elsif ( $format eq 'html' ) {
4123 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4125 sub { my ($d,$a,$s,$w) = @_;
4126 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4135 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4137 &{$column}( &{$f->{fields}->[$i]}(@args),
4138 map { $f->{$_}->[$i] } qw(align span width)
4142 $prefix. join( $separator, @result ). $suffix;
4147 #sub _items_extra_usage_sections {
4149 # my $escape = shift;
4151 # my %sections = ();
4153 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4154 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4156 # next unless $cust_bill_pkg->pkgnum > 0;
4158 # foreach my $section ( keys %usage_class ) {
4160 # my $usage = $cust_bill_pkg->usage($section);
4162 # next unless $usage && $usage > 0;
4164 # $sections{$section} ||= 0;
4165 # $sections{$section} += $usage;
4171 # map { { 'description' => &{$escape}($_),
4172 # 'subtotal' => $sections{$_},
4173 # 'summarized' => '',
4174 # 'tax_section' => '',
4177 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4181 sub _items_extra_usage_sections {
4183 my $conf = $self->conf;
4191 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4193 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4194 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4195 next unless $cust_bill_pkg->pkgnum > 0;
4197 foreach my $classnum ( keys %usage_class ) {
4198 my $section = $usage_class{$classnum}->classname;
4199 $classnums{$section} = $classnum;
4201 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4202 my $amount = $detail->amount;
4203 next unless $amount && $amount > 0;
4205 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4206 $sections{$section}{amount} += $amount; #subtotal
4207 $sections{$section}{calls}++;
4208 $sections{$section}{duration} += $detail->duration;
4210 my $desc = $detail->regionname;
4211 my $description = $desc;
4212 $description = substr($desc, 0, $maxlength). '...'
4213 if $format eq 'latex' && length($desc) > $maxlength;
4215 $lines{$section}{$desc} ||= {
4216 description => &{$escape}($description),
4217 #pkgpart => $part_pkg->pkgpart,
4218 pkgnum => $cust_bill_pkg->pkgnum,
4223 #unit_amount => $cust_bill_pkg->unitrecur,
4224 quantity => $cust_bill_pkg->quantity,
4225 product_code => 'N/A',
4226 ext_description => [],
4229 $lines{$section}{$desc}{amount} += $amount;
4230 $lines{$section}{$desc}{calls}++;
4231 $lines{$section}{$desc}{duration} += $detail->duration;
4237 my %sectionmap = ();
4238 foreach (keys %sections) {
4239 my $usage_class = $usage_class{$classnums{$_}};
4240 $sectionmap{$_} = { 'description' => &{$escape}($_),
4241 'amount' => $sections{$_}{amount}, #subtotal
4242 'calls' => $sections{$_}{calls},
4243 'duration' => $sections{$_}{duration},
4245 'tax_section' => '',
4246 'sort_weight' => $usage_class->weight,
4247 ( $usage_class->format
4248 ? ( map { $_ => $usage_class->$_($format) }
4249 qw( description_generator header_generator total_generator total_line_generator )
4256 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4260 foreach my $section ( keys %lines ) {
4261 foreach my $line ( keys %{$lines{$section}} ) {
4262 my $l = $lines{$section}{$line};
4263 $l->{section} = $sectionmap{$section};
4264 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4265 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4270 return(\@sections, \@lines);
4276 my $end = $self->_date;
4278 # start at date of previous invoice + 1 second or 0 if no previous invoice
4279 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4280 $start = 0 if !$start;
4283 my $cust_main = $self->cust_main;
4284 my @pkgs = $cust_main->all_pkgs;
4285 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4288 foreach my $pkg ( @pkgs ) {
4289 my @h_cust_svc = $pkg->h_cust_svc($end);
4290 foreach my $h_cust_svc ( @h_cust_svc ) {
4291 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4292 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4294 my $inserted = $h_cust_svc->date_inserted;
4295 my $deleted = $h_cust_svc->date_deleted;
4296 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4298 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4300 # DID either activated or ported in; cannot be both for same DID simultaneously
4301 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4302 && (!$phone_inserted->lnp_status
4303 || $phone_inserted->lnp_status eq ''
4304 || $phone_inserted->lnp_status eq 'native')) {
4307 else { # this one not so clean, should probably move to (h_)svc_phone
4308 my $phone_portedin = qsearchs( 'h_svc_phone',
4309 { 'svcnum' => $h_cust_svc->svcnum,
4310 'lnp_status' => 'portedin' },
4311 FS::h_svc_phone->sql_h_searchs($end),
4313 $num_portedin++ if $phone_portedin;
4316 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4317 if($deleted >= $start && $deleted <= $end && $phone_deleted
4318 && (!$phone_deleted->lnp_status
4319 || $phone_deleted->lnp_status ne 'portingout')) {
4322 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4323 && $phone_deleted->lnp_status
4324 && $phone_deleted->lnp_status eq 'portingout') {
4328 # increment usage minutes
4329 if ( $phone_inserted ) {
4330 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4331 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4334 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4337 # don't look at this service again
4338 push @seen, $h_cust_svc->svcnum;
4342 $minutes = sprintf("%d", $minutes);
4343 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4344 . "$num_deactivated Ported-Out: $num_portedout ",
4345 "Total Minutes: $minutes");
4348 sub _items_accountcode_cdr {
4353 my $section = { 'amount' => 0,
4356 'sort_weight' => '',
4358 'description' => 'Usage by Account Code',
4364 my %accountcodes = ();
4366 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4367 next unless $cust_bill_pkg->pkgnum > 0;
4369 my @header = $cust_bill_pkg->details_header;
4370 next unless scalar(@header);
4371 $section->{'header'} = join(',',@header);
4373 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4375 $section->{'header'} = $detail->formatted('format' => $format)
4376 if($detail->detail eq $section->{'header'});
4378 my $accountcode = $detail->accountcode;
4379 next unless $accountcode;
4381 my $amount = $detail->amount;
4382 next unless $amount && $amount > 0;
4384 $accountcodes{$accountcode} ||= {
4385 description => $accountcode,
4392 product_code => 'N/A',
4393 section => $section,
4394 ext_description => [ $section->{'header'} ],
4398 $section->{'amount'} += $amount;
4399 $accountcodes{$accountcode}{'amount'} += $amount;
4400 $accountcodes{$accountcode}{calls}++;
4401 $accountcodes{$accountcode}{duration} += $detail->duration;
4402 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4406 foreach my $l ( values %accountcodes ) {
4407 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4408 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4409 foreach my $sorted_detail ( @sorted_detail ) {
4410 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4412 delete $l->{detail_temp};
4416 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4418 return ($section,\@sorted_lines);
4421 sub _items_svc_phone_sections {
4423 my $conf = $self->conf;
4431 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4433 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4434 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4436 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4437 next unless $cust_bill_pkg->pkgnum > 0;
4439 my @header = $cust_bill_pkg->details_header;
4440 next unless scalar(@header);
4442 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4444 my $phonenum = $detail->phonenum;
4445 next unless $phonenum;
4447 my $amount = $detail->amount;
4448 next unless $amount && $amount > 0;
4450 $sections{$phonenum} ||= { 'amount' => 0,
4453 'sort_weight' => -1,
4454 'phonenum' => $phonenum,
4456 $sections{$phonenum}{amount} += $amount; #subtotal
4457 $sections{$phonenum}{calls}++;
4458 $sections{$phonenum}{duration} += $detail->duration;
4460 my $desc = $detail->regionname;
4461 my $description = $desc;
4462 $description = substr($desc, 0, $maxlength). '...'
4463 if $format eq 'latex' && length($desc) > $maxlength;
4465 $lines{$phonenum}{$desc} ||= {
4466 description => &{$escape}($description),
4467 #pkgpart => $part_pkg->pkgpart,
4475 product_code => 'N/A',
4476 ext_description => [],
4479 $lines{$phonenum}{$desc}{amount} += $amount;
4480 $lines{$phonenum}{$desc}{calls}++;
4481 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4483 my $line = $usage_class{$detail->classnum}->classname;
4484 $sections{"$phonenum $line"} ||=
4488 'sort_weight' => $usage_class{$detail->classnum}->weight,
4489 'phonenum' => $phonenum,
4490 'header' => [ @header ],
4492 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4493 $sections{"$phonenum $line"}{calls}++;
4494 $sections{"$phonenum $line"}{duration} += $detail->duration;
4496 $lines{"$phonenum $line"}{$desc} ||= {
4497 description => &{$escape}($description),
4498 #pkgpart => $part_pkg->pkgpart,
4506 product_code => 'N/A',
4507 ext_description => [],
4510 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4511 $lines{"$phonenum $line"}{$desc}{calls}++;
4512 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4513 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4514 $detail->formatted('format' => $format);
4519 my %sectionmap = ();
4520 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4521 foreach ( keys %sections ) {
4522 my @header = @{ $sections{$_}{header} || [] };
4524 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4525 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4526 my $usage_class = $summary ? $simple : $usage_simple;
4527 my $ending = $summary ? ' usage charges' : '';
4530 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4532 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4533 'amount' => $sections{$_}{amount}, #subtotal
4534 'calls' => $sections{$_}{calls},
4535 'duration' => $sections{$_}{duration},
4537 'tax_section' => '',
4538 'phonenum' => $sections{$_}{phonenum},
4539 'sort_weight' => $sections{$_}{sort_weight},
4540 'post_total' => $summary, #inspire pagebreak
4542 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4543 qw( description_generator
4546 total_line_generator
4553 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4554 $a->{sort_weight} <=> $b->{sort_weight}
4559 foreach my $section ( keys %lines ) {
4560 foreach my $line ( keys %{$lines{$section}} ) {
4561 my $l = $lines{$section}{$line};
4562 $l->{section} = $sectionmap{$section};
4563 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4564 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4569 if($conf->exists('phone_usage_class_summary')) {
4570 # this only works with Latex
4574 # after this, we'll have only two sections per DID:
4575 # Calls Summary and Calls Detail
4576 foreach my $section ( @sections ) {
4577 if($section->{'post_total'}) {
4578 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4579 $section->{'total_line_generator'} = sub { '' };
4580 $section->{'total_generator'} = sub { '' };
4581 $section->{'header_generator'} = sub { '' };
4582 $section->{'description_generator'} = '';
4583 push @newsections, $section;
4584 my %calls_detail = %$section;
4585 $calls_detail{'post_total'} = '';
4586 $calls_detail{'sort_weight'} = '';
4587 $calls_detail{'description_generator'} = sub { '' };
4588 $calls_detail{'header_generator'} = sub {
4589 return ' & Date/Time & Called Number & Duration & Price'
4590 if $format eq 'latex';
4593 $calls_detail{'description'} = 'Calls Detail: '
4594 . $section->{'phonenum'};
4595 push @newsections, \%calls_detail;
4599 # after this, each usage class is collapsed/summarized into a single
4600 # line under the Calls Summary section
4601 foreach my $newsection ( @newsections ) {
4602 if($newsection->{'post_total'}) { # this means Calls Summary
4603 foreach my $section ( @sections ) {
4604 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4605 && !$section->{'post_total'});
4606 my $newdesc = $section->{'description'};
4607 my $tn = $section->{'phonenum'};
4608 $newdesc =~ s/$tn//g;
4609 my $line = { ext_description => [],
4613 calls => $section->{'calls'},
4614 section => $newsection,
4615 duration => $section->{'duration'},
4616 description => $newdesc,
4617 amount => sprintf("%.2f",$section->{'amount'}),
4618 product_code => 'N/A',
4620 push @newlines, $line;
4625 # after this, Calls Details is populated with all CDRs
4626 foreach my $newsection ( @newsections ) {
4627 if(!$newsection->{'post_total'}) { # this means Calls Details
4628 foreach my $line ( @lines ) {
4629 next unless (scalar(@{$line->{'ext_description'}}) &&
4630 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4632 my @extdesc = @{$line->{'ext_description'}};
4634 foreach my $extdesc ( @extdesc ) {
4635 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4636 push @newextdesc, $extdesc;
4638 $line->{'ext_description'} = \@newextdesc;
4639 $line->{'section'} = $newsection;
4640 push @newlines, $line;
4645 return(\@newsections, \@newlines);
4648 return(\@sections, \@lines);
4652 sub _items { # seems to be unused
4655 #my @display = scalar(@_)
4657 # : qw( _items_previous _items_pkg );
4658 # #: qw( _items_pkg );
4659 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4660 my @display = qw( _items_previous _items_pkg );
4663 foreach my $display ( @display ) {
4664 push @b, $self->$display(@_);
4669 sub _items_previous {
4671 my $conf = $self->conf;
4672 my $cust_main = $self->cust_main;
4673 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4675 foreach ( @pr_cust_bill ) {
4676 my $date = $conf->exists('invoice_show_prior_due_date')
4677 ? 'due '. $_->due_date2str($date_format)
4678 : time2str($date_format, $_->_date);
4680 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4681 #'pkgpart' => 'N/A',
4683 'amount' => sprintf("%.2f", $_->owed),
4689 # 'description' => 'Previous Balance',
4690 # #'pkgpart' => 'N/A',
4691 # 'pkgnum' => 'N/A',
4692 # 'amount' => sprintf("%10.2f", $pr_total ),
4693 # 'ext_description' => [ map {
4694 # "Invoice ". $_->invnum.
4695 # " (". time2str("%x",$_->_date). ") ".
4696 # sprintf("%10.2f", $_->owed)
4697 # } @pr_cust_bill ],
4702 =item _items_pkg [ OPTIONS ]
4704 Return line item hashes for each package item on this invoice. Nearly
4707 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4709 The only OPTIONS accepted is 'section', which may point to a hashref
4710 with a key named 'condensed', which may have a true value. If it
4711 does, this method tries to merge identical items into items with
4712 'quantity' equal to the number of items (not the sum of their
4713 separate quantities, for some reason).
4721 warn "$me _items_pkg searching for all package line items\n"
4724 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4726 warn "$me _items_pkg filtering line items\n"
4728 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4730 if ($options{section} && $options{section}->{condensed}) {
4732 warn "$me _items_pkg condensing section\n"
4736 local $Storable::canonical = 1;
4737 foreach ( @items ) {
4739 delete $item->{ref};
4740 delete $item->{ext_description};
4741 my $key = freeze($item);
4742 $itemshash{$key} ||= 0;
4743 $itemshash{$key} ++; # += $item->{quantity};
4745 @items = sort { $a->{description} cmp $b->{description} }
4746 map { my $i = thaw($_);
4747 $i->{quantity} = $itemshash{$_};
4749 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4755 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4762 return 0 unless $a->itemdesc cmp $b->itemdesc;
4763 return -1 if $b->itemdesc eq 'Tax';
4764 return 1 if $a->itemdesc eq 'Tax';
4765 return -1 if $b->itemdesc eq 'Other surcharges';
4766 return 1 if $a->itemdesc eq 'Other surcharges';
4767 $a->itemdesc cmp $b->itemdesc;
4772 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4773 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4776 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4778 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4779 list of hashrefs describing the line items they generate on the invoice.
4781 OPTIONS may include:
4783 format: the invoice format.
4785 escape_function: the function used to escape strings.
4787 DEPRECATED? (expensive, mostly unused?)
4788 format_function: the function used to format CDRs.
4790 section: a hashref containing 'description'; if this is present,
4791 cust_bill_pkg_display records not belonging to this section are
4794 multisection: a flag indicating that this is a multisection invoice,
4795 which does something complicated.
4797 multilocation: a flag to display the location label for the package.
4799 Returns a list of hashrefs, each of which may contain:
4801 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4802 ext_description, which is an arrayref of detail lines to show below
4807 sub _items_cust_bill_pkg {
4809 my $conf = $self->conf;
4810 my $cust_bill_pkgs = shift;
4813 my $format = $opt{format} || '';
4814 my $escape_function = $opt{escape_function} || sub { shift };
4815 my $format_function = $opt{format_function} || '';
4816 my $no_usage = $opt{no_usage} || '';
4817 my $unsquelched = $opt{unsquelched} || ''; #unused
4818 my $section = $opt{section}->{description} if $opt{section};
4819 my $summary_page = $opt{summary_page} || ''; #unused
4820 my $multilocation = $opt{multilocation} || '';
4821 my $multisection = $opt{multisection} || '';
4822 my $discount_show_always = 0;
4824 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4827 my ($s, $r, $u) = ( undef, undef, undef );
4828 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4831 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4832 if ( $_ && !$cust_bill_pkg->hidden ) {
4833 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4834 $_->{amount} =~ s/^\-0\.00$/0.00/;
4835 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4837 if $_->{amount} != 0
4838 || $discount_show_always
4839 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4840 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4846 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4847 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4850 foreach my $display ( grep { defined($section)
4851 ? $_->section eq $section
4854 #grep { !$_->summary || !$summary_page } # bunk!
4855 grep { !$_->summary || $multisection }
4856 $cust_bill_pkg->cust_bill_pkg_display
4860 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4861 $display->billpkgdisplaynum. "\n"
4864 my $type = $display->type;
4866 my $desc = $cust_bill_pkg->desc;
4867 $desc = substr($desc, 0, $maxlength). '...'
4868 if $format eq 'latex' && length($desc) > $maxlength;
4870 my %details_opt = ( 'format' => $format,
4871 'escape_function' => $escape_function,
4872 'format_function' => $format_function,
4873 'no_usage' => $opt{'no_usage'},
4876 if ( $cust_bill_pkg->pkgnum > 0 ) {
4878 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4881 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4883 # start/end dates for invoice formats that do nonstandard
4885 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4887 if ( (!$type || $type eq 'S')
4888 && ( $cust_bill_pkg->setup != 0
4889 || $cust_bill_pkg->setup_show_zero
4894 warn "$me _items_cust_bill_pkg adding setup\n"
4897 my $description = $desc;
4898 $description .= ' Setup'
4899 if $cust_bill_pkg->recur != 0
4900 || $discount_show_always
4901 || $cust_bill_pkg->recur_show_zero;
4904 unless ( $cust_pkg->part_pkg->hide_svc_detail
4905 || $cust_bill_pkg->hidden )
4908 push @d, map &{$escape_function}($_),
4909 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4910 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4912 if ( $multilocation ) {
4913 my $loc = $cust_pkg->location_label;
4914 $loc = substr($loc, 0, $maxlength). '...'
4915 if $format eq 'latex' && length($loc) > $maxlength;
4916 push @d, &{$escape_function}($loc);
4919 } #unless hiding service details
4921 push @d, $cust_bill_pkg->details(%details_opt)
4922 if $cust_bill_pkg->recur == 0;
4924 if ( $cust_bill_pkg->hidden ) {
4925 $s->{amount} += $cust_bill_pkg->setup;
4926 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4927 push @{ $s->{ext_description} }, @d;
4931 description => $description,
4932 #pkgpart => $part_pkg->pkgpart,
4933 pkgnum => $cust_bill_pkg->pkgnum,
4934 amount => $cust_bill_pkg->setup,
4935 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4936 unit_amount => $cust_bill_pkg->unitsetup,
4937 quantity => $cust_bill_pkg->quantity,
4938 ext_description => \@d,
4944 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4946 $cust_bill_pkg->recur != 0
4947 || $cust_bill_pkg->setup == 0
4948 || $discount_show_always
4949 || $cust_bill_pkg->recur_show_zero
4954 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4957 my $is_summary = $display->summary;
4958 my $description = ($is_summary && $type && $type eq 'U')
4959 ? "Usage charges" : $desc;
4961 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4962 " - ". time2str($date_format, $cust_bill_pkg->edate).
4964 unless $conf->exists('disable_line_item_date_ranges')
4965 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4968 my @seconds = (); # for display of usage info
4970 #at least until cust_bill_pkg has "past" ranges in addition to
4971 #the "future" sdate/edate ones... see #3032
4972 my @dates = ( $self->_date );
4973 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4974 push @dates, $prev->sdate if $prev;
4975 push @dates, undef if !$prev;
4977 unless ( $cust_pkg->part_pkg->hide_svc_detail
4978 || $cust_bill_pkg->itemdesc
4979 || $cust_bill_pkg->hidden
4980 || $is_summary && $type && $type eq 'U' )
4983 warn "$me _items_cust_bill_pkg adding service details\n"
4986 push @d, map &{$escape_function}($_),
4987 $cust_pkg->h_labels_short(@dates, 'I')
4988 #$cust_bill_pkg->edate,
4989 #$cust_bill_pkg->sdate)
4990 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4992 warn "$me _items_cust_bill_pkg done adding service details\n"
4995 if ( $multilocation ) {
4996 my $loc = $cust_pkg->location_label;
4997 $loc = substr($loc, 0, $maxlength). '...'
4998 if $format eq 'latex' && length($loc) > $maxlength;
4999 push @d, &{$escape_function}($loc);
5002 # Display of seconds_since_sqlradacct:
5003 # On the invoice, when processing @detail_items, look for a field
5004 # named 'seconds'. This will contain total seconds for each
5005 # service, in the same order as @ext_description. For services
5006 # that don't support this it will show undef.
5007 if ( $conf->exists('svc_acct-usage_seconds')
5008 and ! $cust_bill_pkg->pkgpart_override ) {
5009 foreach my $cust_svc (
5010 $cust_pkg->h_cust_svc(@dates, 'I')
5013 # eval because not having any part_export_usage exports
5014 # is a fatal error, last_bill/_date because that's how
5015 # sqlradius_hour billing does it
5017 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
5019 push @seconds, $sec;
5021 } #if svc_acct-usage_seconds
5025 unless ( $is_summary ) {
5026 warn "$me _items_cust_bill_pkg adding details\n"
5029 #instead of omitting details entirely in this case (unwanted side
5030 # effects), just omit CDRs
5031 $details_opt{'no_usage'} = 1
5032 if $type && $type eq 'R';
5034 push @d, $cust_bill_pkg->details(%details_opt);
5037 warn "$me _items_cust_bill_pkg calculating amount\n"
5042 $amount = $cust_bill_pkg->recur;
5043 } elsif ($type eq 'R') {
5044 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5045 } elsif ($type eq 'U') {
5046 $amount = $cust_bill_pkg->usage;
5049 if ( !$type || $type eq 'R' ) {
5051 warn "$me _items_cust_bill_pkg adding recur\n"
5054 if ( $cust_bill_pkg->hidden ) {
5055 $r->{amount} += $amount;
5056 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5057 push @{ $r->{ext_description} }, @d;
5060 description => $description,
5061 #pkgpart => $part_pkg->pkgpart,
5062 pkgnum => $cust_bill_pkg->pkgnum,
5064 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5065 unit_amount => $cust_bill_pkg->unitrecur,
5066 quantity => $cust_bill_pkg->quantity,
5068 ext_description => \@d,
5070 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5073 } else { # $type eq 'U'
5075 warn "$me _items_cust_bill_pkg adding usage\n"
5078 if ( $cust_bill_pkg->hidden ) {
5079 $u->{amount} += $amount;
5080 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5081 push @{ $u->{ext_description} }, @d;
5084 description => $description,
5085 #pkgpart => $part_pkg->pkgpart,
5086 pkgnum => $cust_bill_pkg->pkgnum,
5088 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5089 unit_amount => $cust_bill_pkg->unitrecur,
5090 quantity => $cust_bill_pkg->quantity,
5092 ext_description => \@d,
5097 } # recurring or usage with recurring charge
5099 } else { #pkgnum tax or one-shot line item (??)
5101 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5104 if ( $cust_bill_pkg->setup != 0 ) {
5106 'description' => $desc,
5107 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5110 if ( $cust_bill_pkg->recur != 0 ) {
5112 'description' => "$desc (".
5113 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5114 time2str($date_format, $cust_bill_pkg->edate). ')',
5115 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5123 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5124 && $conf->exists('discount-show-always'));
5128 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5130 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5131 $_->{amount} =~ s/^\-0\.00$/0.00/;
5132 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5134 if $_->{amount} != 0
5135 || $discount_show_always
5136 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5137 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5141 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5148 sub _items_credits {
5149 my( $self, %opt ) = @_;
5150 my $trim_len = $opt{'trim_len'} || 60;
5154 foreach ( $self->cust_credited ) {
5156 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5158 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5159 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5160 $reason = " ($reason) " if $reason;
5163 #'description' => 'Credit ref\#'. $_->crednum.
5164 # " (". time2str("%x",$_->cust_credit->_date) .")".
5166 'description' => $self->mt('Credit applied').' '.
5167 time2str($date_format,$_->cust_credit->_date). $reason,
5168 'amount' => sprintf("%.2f",$_->amount),
5176 sub _items_payments {
5180 #get & print payments
5181 foreach ( $self->cust_bill_pay ) {
5183 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5186 'description' => $self->mt('Payment received').' '.
5187 time2str($date_format,$_->cust_pay->_date ),
5188 'amount' => sprintf("%.2f", $_->amount )
5196 =item _items_discounts_avail
5198 Returns an array of line item hashrefs representing available term discounts
5199 for this invoice. This makes the same assumptions that apply to term
5200 discounts in general: that the package is billed monthly, at a flat rate,
5201 with no usage charges. A prorated first month will be handled, as will
5202 a setup fee if the discount is allowed to apply to setup fees.
5206 sub _items_discounts_avail {
5209 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5211 my ($previous_balance) = $self->previous;
5213 foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5214 $terms{$_->months} = {
5216 base => $previous_balance || 0, # pre-discount sum of charges
5217 discounted => $previous_balance || 0, # post-discount sum
5218 list_pkgnums => 0, # whether any packages are not discounted
5221 foreach my $months (keys %terms) {
5222 my $hash = $terms{$months};
5224 # tricky, because packages may not all be eligible for the same discounts
5225 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5226 my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5227 my $part_pkg = $cust_pkg->part_pkg or next;
5228 my $freq = $part_pkg->freq;
5229 my $setup = $cust_bill_pkg->setup || 0;
5230 my $recur = $cust_bill_pkg->recur || 0;
5232 if ( $freq eq '1' ) { #monthly
5233 my $permonth = $part_pkg->base_recur_permonth || 0;
5235 my ($discount) = grep { $_->months == $months }
5236 map { $_->discount } $part_pkg->part_pkg_discount;
5238 $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5243 if ( $discount->setup ) {
5244 $discountable += $setup;
5247 $hash->{discounted} += $setup;
5250 if ( $discount->percent ) {
5251 $discountable += $months * $permonth;
5252 $discountable -= ($discountable * $discount->percent / 100);
5253 $discountable -= ($permonth - $recur); # correct for prorate
5254 $hash->{discounted} += $discountable;
5257 $discountable += $recur;
5258 $discountable -= $discount->amount * $recur/$permonth;
5260 $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5263 $hash->{discounted} += $discountable;
5264 push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5267 $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5268 $hash->{list_pkgnums} = 1;
5271 else { # all non-monthly packages: include current charges only
5272 $hash->{discounted} += $setup + $recur;
5273 $hash->{base} += $setup + $recur;
5274 $hash->{list_pkgnums} = 1;
5276 } #foreach $cust_bill_pkg
5278 # don't show this line if no packages have discounts at this term
5279 # or if there are no new charges to apply the discount to
5280 delete $terms{$months} if $hash->{base} == $hash->{discounted}
5281 or $hash->{base} == 0;
5285 $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5287 foreach my $months (keys %terms) {
5288 my $hash = $terms{$months};
5289 my $term_total = sprintf('%.2f', $hash->{discounted});
5290 # possibly shouldn't include previous balance in these?
5291 my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5292 my $permonth = sprintf('%.2f', $term_total / $months);
5294 $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5297 $hash->{amount} = $self->mt('[_1] ([_2] per month)',
5298 $term_total, $money_char.$permonth
5302 if ( $list_pkgnums ) {
5303 push @detail, $self->mt('discount on item'). ' '.
5304 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5306 $hash->{ext_description} = join ', ', @detail;
5309 map { $terms{$_} } sort {$b <=> $a} keys %terms;
5312 =item call_details [ OPTION => VALUE ... ]
5314 Returns an array of CSV strings representing the call details for this invoice
5315 The only option available is the boolean prepend_billed_number
5320 my ($self, %opt) = @_;
5322 my $format_function = sub { shift };
5324 if ($opt{prepend_billed_number}) {
5325 $format_function = sub {
5329 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5334 my @details = map { $_->details( 'format_function' => $format_function,
5335 'escape_function' => sub{ return() },
5339 $self->cust_bill_pkg;
5340 my $header = $details[0];
5341 ( $header, grep { $_ ne $header } @details );
5351 =item process_reprint
5355 sub process_reprint {
5356 process_re_X('print', @_);
5359 =item process_reemail
5363 sub process_reemail {
5364 process_re_X('email', @_);
5372 process_re_X('fax', @_);
5380 process_re_X('ftp', @_);
5387 sub process_respool {
5388 process_re_X('spool', @_);
5391 use Storable qw(thaw);
5395 my( $method, $job ) = ( shift, shift );
5396 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5398 my $param = thaw(decode_base64(shift));
5399 warn Dumper($param) if $DEBUG;
5410 my($method, $job, %param ) = @_;
5412 warn "re_X $method for job $job with param:\n".
5413 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5416 #some false laziness w/search/cust_bill.html
5418 my $orderby = 'ORDER BY cust_bill._date';
5420 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5422 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5424 my @cust_bill = qsearch( {
5425 #'select' => "cust_bill.*",
5426 'table' => 'cust_bill',
5427 'addl_from' => $addl_from,
5429 'extra_sql' => $extra_sql,
5430 'order_by' => $orderby,
5434 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5436 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5439 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5440 foreach my $cust_bill ( @cust_bill ) {
5441 $cust_bill->$method();
5443 if ( $job ) { #progressbar foo
5445 if ( time - $min_sec > $last ) {
5446 my $error = $job->update_statustext(
5447 int( 100 * $num / scalar(@cust_bill) )
5449 die $error if $error;
5460 =head1 CLASS METHODS
5466 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5471 my ($class, $start, $end) = @_;
5473 $class->paid_sql($start, $end). ' - '.
5474 $class->credited_sql($start, $end);
5479 Returns an SQL fragment to retreive the net amount (charged minus credited).
5484 my ($class, $start, $end) = @_;
5485 'charged - '. $class->credited_sql($start, $end);
5490 Returns an SQL fragment to retreive the amount paid against this invoice.
5495 my ($class, $start, $end) = @_;
5496 $start &&= "AND cust_bill_pay._date <= $start";
5497 $end &&= "AND cust_bill_pay._date > $end";
5498 $start = '' unless defined($start);
5499 $end = '' unless defined($end);
5500 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5501 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5506 Returns an SQL fragment to retreive the amount credited against this invoice.
5511 my ($class, $start, $end) = @_;
5512 $start &&= "AND cust_credit_bill._date <= $start";
5513 $end &&= "AND cust_credit_bill._date > $end";
5514 $start = '' unless defined($start);
5515 $end = '' unless defined($end);
5516 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5517 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5522 Returns an SQL fragment to retrieve the due date of an invoice.
5523 Currently only supported on PostgreSQL.
5528 my $conf = new FS::Conf;
5532 cust_bill.invoice_terms,
5533 cust_main.invoice_terms,
5534 \''.($conf->config('invoice_default_terms') || '').'\'
5535 ), E\'Net (\\\\d+)\'
5537 ) * 86400 + cust_bill._date'
5540 =item search_sql_where HASHREF
5542 Class method which returns an SQL WHERE fragment to search for parameters
5543 specified in HASHREF. Valid parameters are
5549 List reference of start date, end date, as UNIX timestamps.
5559 List reference of charged limits (exclusive).
5563 List reference of charged limits (exclusive).
5567 flag, return open invoices only
5571 flag, return net invoices only
5575 =item newest_percust
5579 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5583 sub search_sql_where {
5584 my($class, $param) = @_;
5586 warn "$me search_sql_where called with params: \n".
5587 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5593 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5594 push @search, "cust_main.agentnum = $1";
5598 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5599 push @search, "cust_bill.custnum = $1";
5603 if ( $param->{_date} ) {
5604 my($beginning, $ending) = @{$param->{_date}};
5606 push @search, "cust_bill._date >= $beginning",
5607 "cust_bill._date < $ending";
5611 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5612 push @search, "cust_bill.invnum >= $1";
5614 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5615 push @search, "cust_bill.invnum <= $1";
5619 if ( $param->{charged} ) {
5620 my @charged = ref($param->{charged})
5621 ? @{ $param->{charged} }
5622 : ($param->{charged});
5624 push @search, map { s/^charged/cust_bill.charged/; $_; }
5628 my $owed_sql = FS::cust_bill->owed_sql;
5631 if ( $param->{owed} ) {
5632 my @owed = ref($param->{owed})
5633 ? @{ $param->{owed} }
5635 push @search, map { s/^owed/$owed_sql/; $_; }
5640 push @search, "0 != $owed_sql"
5641 if $param->{'open'};
5642 push @search, '0 != '. FS::cust_bill->net_sql
5646 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5647 if $param->{'days'};
5650 if ( $param->{'newest_percust'} ) {
5652 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5653 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5655 my @newest_where = map { my $x = $_;
5656 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5659 grep ! /^cust_main./, @search;
5660 my $newest_where = scalar(@newest_where)
5661 ? ' AND '. join(' AND ', @newest_where)
5665 push @search, "cust_bill._date = (
5666 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5667 WHERE newest_cust_bill.custnum = cust_bill.custnum
5673 #agent virtualization
5674 my $curuser = $FS::CurrentUser::CurrentUser;
5675 if ( $curuser->username eq 'fs_queue'
5676 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5678 my $newuser = qsearchs('access_user', {
5679 'username' => $username,
5683 $curuser = $newuser;
5685 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5688 push @search, $curuser->agentnums_sql;
5690 join(' AND ', @search );
5702 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5703 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base