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(
254 foreach my $linked ( $self->$table() ) {
255 my $error = $linked->delete;
257 $dbh->rollback if $oldAutoCommit;
264 my $error = $self->SUPER::delete(@_);
266 $dbh->rollback if $oldAutoCommit;
270 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
276 =item replace [ OLD_RECORD ]
278 You can, but probably shouldn't modify invoices...
280 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
281 supplied, replaces this record. If there is an error, returns the error,
282 otherwise returns false.
286 #replace can be inherited from Record.pm
288 # replace_check is now the preferred way to #implement replace data checks
289 # (so $object->replace() works without an argument)
292 my( $new, $old ) = ( shift, shift );
293 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
294 #return "Can't change _date!" unless $old->_date eq $new->_date;
295 return "Can't change _date" unless $old->_date == $new->_date;
296 return "Can't change charged" unless $old->charged == $new->charged
297 || $old->charged == 0
298 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
304 =item add_cc_surcharge
310 sub add_cc_surcharge {
311 my ($self, $pkgnum, $amount) = (shift, shift, shift);
314 my $cust_bill_pkg = new FS::cust_bill_pkg({
315 'invnum' => $self->invnum,
319 $error = $cust_bill_pkg->insert;
320 return $error if $error;
322 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
323 $self->charged($self->charged+$amount);
324 $error = $self->replace;
325 return $error if $error;
327 $self->apply_payments_and_credits;
333 Checks all fields to make sure this is a valid invoice. If there is an error,
334 returns the error, otherwise returns false. Called by the insert and replace
343 $self->ut_numbern('invnum')
344 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
345 || $self->ut_numbern('_date')
346 || $self->ut_money('charged')
347 || $self->ut_numbern('printed')
348 || $self->ut_enum('closed', [ '', 'Y' ])
349 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
350 || $self->ut_numbern('agent_invid') #varchar?
352 return $error if $error;
354 $self->_date(time) unless $self->_date;
356 $self->printed(0) if $self->printed eq '';
363 Returns the displayed invoice number for this invoice: agent_invid if
364 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
370 my $conf = $self->conf;
371 if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
372 return $self->agent_invid;
374 return $self->invnum;
380 Returns a list consisting of the total previous balance for this customer,
381 followed by the previous outstanding invoices (as FS::cust_bill objects also).
388 my @cust_bill = sort { $a->_date <=> $b->_date }
389 grep { $_->owed != 0 && $_->_date < $self->_date }
390 qsearch( 'cust_bill', { 'custnum' => $self->custnum } )
392 foreach ( @cust_bill ) { $total += $_->owed; }
398 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
405 { 'table' => 'cust_bill_pkg',
406 'hashref' => { 'invnum' => $self->invnum },
407 'order_by' => 'ORDER BY billpkgnum',
412 =item cust_bill_pkg_pkgnum PKGNUM
414 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
419 sub cust_bill_pkg_pkgnum {
420 my( $self, $pkgnum ) = @_;
422 { 'table' => 'cust_bill_pkg',
423 'hashref' => { 'invnum' => $self->invnum,
426 'order_by' => 'ORDER BY billpkgnum',
433 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
440 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
441 $self->cust_bill_pkg;
443 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
448 Returns true if any of the packages (or their definitions) corresponding to the
449 line items for this invoice have the no_auto flag set.
455 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
458 =item open_cust_bill_pkg
460 Returns the open line items for this invoice.
462 Note that cust_bill_pkg with both setup and recur fees are returned as two
463 separate line items, each with only one fee.
467 # modeled after cust_main::open_cust_bill
468 sub open_cust_bill_pkg {
471 # grep { $_->owed > 0 } $self->cust_bill_pkg
473 my %other = ( 'recur' => 'setup',
474 'setup' => 'recur', );
476 foreach my $field ( qw( recur setup )) {
477 push @open, map { $_->set( $other{$field}, 0 ); $_; }
478 grep { $_->owed($field) > 0 }
479 $self->cust_bill_pkg;
485 =item cust_bill_event
487 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
491 sub cust_bill_event {
493 qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
496 =item num_cust_bill_event
498 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
502 sub num_cust_bill_event {
505 "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
506 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
507 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
508 $sth->fetchrow_arrayref->[0];
513 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
517 #false laziness w/cust_pkg.pm
521 'table' => 'cust_event',
522 'addl_from' => 'JOIN part_event USING ( eventpart )',
523 'hashref' => { 'tablenum' => $self->invnum },
524 'extra_sql' => " AND eventtable = 'cust_bill' ",
530 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
534 #false laziness w/cust_pkg.pm
538 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
539 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
540 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
541 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
542 $sth->fetchrow_arrayref->[0];
547 Returns the customer (see L<FS::cust_main>) for this invoice.
553 qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
556 =item cust_suspend_if_balance_over AMOUNT
558 Suspends the customer associated with this invoice if the total amount owed on
559 this invoice and all older invoices is greater than the specified amount.
561 Returns a list: an empty list on success or a list of errors.
565 sub cust_suspend_if_balance_over {
566 my( $self, $amount ) = ( shift, shift );
567 my $cust_main = $self->cust_main;
568 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
571 $cust_main->suspend(@_);
577 Depreciated. See the cust_credited method.
579 #Returns a list consisting of the total previous credited (see
580 #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
581 #outstanding credits (FS::cust_credit objects).
587 croak "FS::cust_bill->cust_credit depreciated; see ".
588 "FS::cust_bill->cust_credit_bill";
591 #my @cust_credit = sort { $a->_date <=> $b->_date }
592 # grep { $_->credited != 0 && $_->_date < $self->_date }
593 # qsearch('cust_credit', { 'custnum' => $self->custnum } )
595 #foreach (@cust_credit) { $total += $_->credited; }
596 #$total, @cust_credit;
601 Depreciated. See the cust_bill_pay method.
603 #Returns all payments (see L<FS::cust_pay>) for this invoice.
609 croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
611 #sort { $a->_date <=> $b->_date }
612 # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
618 qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
621 sub cust_bill_pay_batch {
623 qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
628 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
634 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
635 sort { $a->_date <=> $b->_date }
636 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
641 =item cust_credit_bill
643 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
649 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
650 sort { $a->_date <=> $b->_date }
651 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
655 sub cust_credit_bill {
656 shift->cust_credited(@_);
659 #=item cust_bill_pay_pkgnum PKGNUM
661 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
662 #with matching pkgnum.
666 #sub cust_bill_pay_pkgnum {
667 # my( $self, $pkgnum ) = @_;
668 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
669 # sort { $a->_date <=> $b->_date }
670 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
671 # 'pkgnum' => $pkgnum,
676 =item cust_bill_pay_pkg PKGNUM
678 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
679 applied against the matching pkgnum.
683 sub cust_bill_pay_pkg {
684 my( $self, $pkgnum ) = @_;
687 'select' => 'cust_bill_pay_pkg.*',
688 'table' => 'cust_bill_pay_pkg',
689 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
690 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
691 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
692 " AND cust_bill_pkg.pkgnum = $pkgnum",
697 #=item cust_credited_pkgnum PKGNUM
699 #=item cust_credit_bill_pkgnum PKGNUM
701 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
702 #with matching pkgnum.
706 #sub cust_credited_pkgnum {
707 # my( $self, $pkgnum ) = @_;
708 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
709 # sort { $a->_date <=> $b->_date }
710 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
711 # 'pkgnum' => $pkgnum,
716 #sub cust_credit_bill_pkgnum {
717 # shift->cust_credited_pkgnum(@_);
720 =item cust_credit_bill_pkg PKGNUM
722 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
723 applied against the matching pkgnum.
727 sub cust_credit_bill_pkg {
728 my( $self, $pkgnum ) = @_;
731 'select' => 'cust_credit_bill_pkg.*',
732 'table' => 'cust_credit_bill_pkg',
733 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
734 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
735 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
736 " AND cust_bill_pkg.pkgnum = $pkgnum",
741 =item cust_bill_batch
743 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
747 sub cust_bill_batch {
749 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
754 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
761 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
763 foreach (@taxlines) { $total += $_->setup; }
769 Returns the amount owed (still outstanding) on this invoice, which is charged
770 minus all payment applications (see L<FS::cust_bill_pay>) and credit
771 applications (see L<FS::cust_credit_bill>).
777 my $balance = $self->charged;
778 $balance -= $_->amount foreach ( $self->cust_bill_pay );
779 $balance -= $_->amount foreach ( $self->cust_credited );
780 $balance = sprintf( "%.2f", $balance);
781 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
786 my( $self, $pkgnum ) = @_;
788 #my $balance = $self->charged;
790 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
792 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
793 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
795 $balance = sprintf( "%.2f", $balance);
796 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
800 =item apply_payments_and_credits [ OPTION => VALUE ... ]
802 Applies unapplied payments and credits to this invoice.
804 A hash of optional arguments may be passed. Currently "manual" is supported.
805 If true, a payment receipt is sent instead of a statement when
806 'payment_receipt_email' configuration option is set.
808 If there is an error, returns the error, otherwise returns false.
812 sub apply_payments_and_credits {
813 my( $self, %options ) = @_;
814 my $conf = $self->conf;
816 local $SIG{HUP} = 'IGNORE';
817 local $SIG{INT} = 'IGNORE';
818 local $SIG{QUIT} = 'IGNORE';
819 local $SIG{TERM} = 'IGNORE';
820 local $SIG{TSTP} = 'IGNORE';
821 local $SIG{PIPE} = 'IGNORE';
823 my $oldAutoCommit = $FS::UID::AutoCommit;
824 local $FS::UID::AutoCommit = 0;
827 $self->select_for_update; #mutex
829 my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
830 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
832 if ( $conf->exists('pkg-balances') ) {
833 # limit @payments & @credits to those w/ a pkgnum grepped from $self
834 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
835 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
836 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
839 while ( $self->owed > 0 and ( @payments || @credits ) ) {
842 if ( @payments && @credits ) {
844 #decide which goes first by weight of top (unapplied) line item
846 my @open_lineitems = $self->open_cust_bill_pkg;
849 max( map { $_->part_pkg->pay_weight || 0 }
854 my $max_credit_weight =
855 max( map { $_->part_pkg->credit_weight || 0 }
861 #if both are the same... payments first? it has to be something
862 if ( $max_pay_weight >= $max_credit_weight ) {
868 } elsif ( @payments ) {
870 } elsif ( @credits ) {
873 die "guru meditation #12 and 35";
877 if ( $app eq 'pay' ) {
879 my $payment = shift @payments;
880 $unapp_amount = $payment->unapplied;
881 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
882 $app->pkgnum( $payment->pkgnum )
883 if $conf->exists('pkg-balances') && $payment->pkgnum;
885 } elsif ( $app eq 'credit' ) {
887 my $credit = shift @credits;
888 $unapp_amount = $credit->credited;
889 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
890 $app->pkgnum( $credit->pkgnum )
891 if $conf->exists('pkg-balances') && $credit->pkgnum;
894 die "guru meditation #12 and 35";
898 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
899 warn "owed_pkgnum ". $app->pkgnum;
900 $owed = $self->owed_pkgnum($app->pkgnum);
904 next unless $owed > 0;
906 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
907 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
909 $app->invnum( $self->invnum );
911 my $error = $app->insert(%options);
913 $dbh->rollback if $oldAutoCommit;
914 return "Error inserting ". $app->table. " record: $error";
916 die $error if $error;
920 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
925 =item generate_email OPTION => VALUE ...
933 sender address, required
937 alternate template name, optional
941 text attachment arrayref, optional
945 email subject, optional
949 notice name instead of "Invoice", optional
953 Returns an argument list to be passed to L<FS::Misc::send_email>.
963 my $conf = $self->conf;
965 my $me = '[FS::cust_bill::generate_email]';
968 'from' => $args{'from'},
969 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
973 'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
974 'template' => $args{'template'},
975 'notice_name' => ( $args{'notice_name'} || 'Invoice' ),
976 'no_coupon' => $args{'no_coupon'},
979 my $cust_main = $self->cust_main;
981 if (ref($args{'to'}) eq 'ARRAY') {
982 $return{'to'} = $args{'to'};
984 $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
985 $cust_main->invoicing_list
989 if ( $conf->exists('invoice_html') ) {
991 warn "$me creating HTML/text multipart message"
994 $return{'nobody'} = 1;
996 my $alternative = build MIME::Entity
997 'Type' => 'multipart/alternative',
998 #'Encoding' => '7bit',
999 'Disposition' => 'inline'
1003 if ( $conf->exists('invoice_email_pdf')
1004 and scalar($conf->config('invoice_email_pdf_note')) ) {
1006 warn "$me using 'invoice_email_pdf_note' in multipart message"
1008 $data = [ map { $_ . "\n" }
1009 $conf->config('invoice_email_pdf_note')
1014 warn "$me not using 'invoice_email_pdf_note' in multipart message"
1016 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1017 $data = $args{'print_text'};
1019 $data = [ $self->print_text(\%opt) ];
1024 $alternative->attach(
1025 'Type' => 'text/plain',
1026 'Encoding' => 'quoted-printable',
1027 #'Encoding' => '7bit',
1029 'Disposition' => 'inline',
1032 $args{'from'} =~ /\@([\w\.\-]+)/;
1033 my $from = $1 || 'example.com';
1034 my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1037 my $agentnum = $cust_main->agentnum;
1038 if ( defined($args{'template'}) && length($args{'template'})
1039 && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
1042 $logo = 'logo_'. $args{'template'}. '.png';
1046 my $image_data = $conf->config_binary( $logo, $agentnum);
1048 my $image = build MIME::Entity
1049 'Type' => 'image/png',
1050 'Encoding' => 'base64',
1051 'Data' => $image_data,
1052 'Filename' => 'logo.png',
1053 'Content-ID' => "<$content_id>",
1057 if($conf->exists('invoice-barcode')){
1058 my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
1059 $barcode = build MIME::Entity
1060 'Type' => 'image/png',
1061 'Encoding' => 'base64',
1062 'Data' => $self->invoice_barcode(0),
1063 'Filename' => 'barcode.png',
1064 'Content-ID' => "<$barcode_content_id>",
1066 $opt{'barcode_cid'} = $barcode_content_id;
1069 $alternative->attach(
1070 'Type' => 'text/html',
1071 'Encoding' => 'quoted-printable',
1072 'Data' => [ '<html>',
1075 ' '. encode_entities($return{'subject'}),
1078 ' <body bgcolor="#e8e8e8">',
1079 $self->print_html({ 'cid'=>$content_id, %opt }),
1083 'Disposition' => 'inline',
1084 #'Filename' => 'invoice.pdf',
1087 my @otherparts = ();
1088 if ( $cust_main->email_csv_cdr ) {
1090 push @otherparts, build MIME::Entity
1091 'Type' => 'text/csv',
1092 'Encoding' => '7bit',
1093 'Data' => [ map { "$_\n" }
1094 $self->call_details('prepend_billed_number' => 1)
1096 'Disposition' => 'attachment',
1097 'Filename' => 'usage-'. $self->invnum. '.csv',
1102 if ( $conf->exists('invoice_email_pdf') ) {
1107 # multipart/alternative
1113 my $related = build MIME::Entity 'Type' => 'multipart/related',
1114 'Encoding' => '7bit';
1116 #false laziness w/Misc::send_email
1117 $related->head->replace('Content-type',
1118 $related->mime_type.
1119 '; boundary="'. $related->head->multipart_boundary. '"'.
1120 '; type=multipart/alternative'
1123 $related->add_part($alternative);
1125 $related->add_part($image);
1127 my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1129 $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1133 #no other attachment:
1135 # multipart/alternative
1140 $return{'content-type'} = 'multipart/related';
1141 if($conf->exists('invoice-barcode')){
1142 $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
1145 $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1147 $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1148 #$return{'disposition'} = 'inline';
1154 if ( $conf->exists('invoice_email_pdf') ) {
1155 warn "$me creating PDF attachment"
1158 #mime parts arguments a la MIME::Entity->build().
1159 $return{'mimeparts'} = [
1160 { $self->mimebuild_pdf(\%opt) }
1164 if ( $conf->exists('invoice_email_pdf')
1165 and scalar($conf->config('invoice_email_pdf_note')) ) {
1167 warn "$me using 'invoice_email_pdf_note'"
1169 $return{'body'} = [ map { $_ . "\n" }
1170 $conf->config('invoice_email_pdf_note')
1175 warn "$me not using 'invoice_email_pdf_note'"
1177 if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1178 $return{'body'} = $args{'print_text'};
1180 $return{'body'} = [ $self->print_text(\%opt) ];
1193 Returns a list suitable for passing to MIME::Entity->build(), representing
1194 this invoice as PDF attachment.
1201 'Type' => 'application/pdf',
1202 'Encoding' => 'base64',
1203 'Data' => [ $self->print_pdf(@_) ],
1204 'Disposition' => 'attachment',
1205 'Filename' => 'invoice-'. $self->invnum. '.pdf',
1209 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1211 Sends this invoice to the destinations configured for this customer: sends
1212 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1214 Options can be passed as a hashref (recommended) or as a list of up to
1215 four values for templatename, agentnum, invoice_from and amount.
1217 I<template>, if specified, is the name of a suffix for alternate invoices.
1219 I<agentnum>, if specified, means that this invoice will only be sent for customers
1220 of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a
1221 single agent) or an arrayref of agentnums.
1223 I<invoice_from>, if specified, overrides the default email invoice From: address.
1225 I<amount>, if specified, only sends the invoice if the total amount owed on this
1226 invoice and all older invoices is greater than the specified amount.
1228 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1232 sub queueable_send {
1235 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1236 or die "invalid invoice number: " . $opt{invnum};
1238 my @args = ( $opt{template}, $opt{agentnum} );
1239 push @args, $opt{invoice_from}
1240 if exists($opt{invoice_from}) && $opt{invoice_from};
1242 my $error = $self->send( @args );
1243 die $error if $error;
1249 my $conf = $self->conf;
1251 my( $template, $invoice_from, $notice_name );
1253 my $balance_over = 0;
1257 $template = $opt->{'template'} || '';
1258 if ( $agentnums = $opt->{'agentnum'} ) {
1259 $agentnums = [ $agentnums ] unless ref($agentnums);
1261 $invoice_from = $opt->{'invoice_from'};
1262 $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1263 $notice_name = $opt->{'notice_name'};
1265 $template = scalar(@_) ? shift : '';
1266 if ( scalar(@_) && $_[0] ) {
1267 $agentnums = ref($_[0]) ? shift : [ shift ];
1269 $invoice_from = shift if scalar(@_);
1270 $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1273 return 'N/A' unless ! $agentnums
1274 or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1277 unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1279 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1280 $conf->config('invoice_from', $self->cust_main->agentnum );
1283 'template' => $template,
1284 'invoice_from' => $invoice_from,
1285 'notice_name' => ( $notice_name || 'Invoice' ),
1288 my @invoicing_list = $self->cust_main->invoicing_list;
1290 #$self->email_invoice(\%opt)
1292 if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1294 #$self->print_invoice(\%opt)
1296 if grep { $_ eq 'POST' } @invoicing_list; #postal
1298 $self->fax_invoice(\%opt)
1299 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1305 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ]
1307 Emails this invoice.
1309 Options can be passed as a hashref (recommended) or as a list of up to
1310 two values for templatename and invoice_from.
1312 I<template>, if specified, is the name of a suffix for alternate invoices.
1314 I<invoice_from>, if specified, overrides the default email invoice From: address.
1316 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1320 sub queueable_email {
1323 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1324 or die "invalid invoice number: " . $opt{invnum};
1326 my %args = ( 'template' => $opt{template} );
1327 $args{$_} = $opt{$_}
1328 foreach grep { exists($opt{$_}) && $opt{$_} }
1329 qw( invoice_from notice_name no_coupon );
1331 my $error = $self->email( \%args );
1332 die $error if $error;
1336 #sub email_invoice {
1339 my $conf = $self->conf;
1341 my( $template, $invoice_from, $notice_name, $no_coupon );
1344 $template = $opt->{'template'} || '';
1345 $invoice_from = $opt->{'invoice_from'};
1346 $notice_name = $opt->{'notice_name'} || 'Invoice';
1347 $no_coupon = $opt->{'no_coupon'} || 0;
1349 $template = scalar(@_) ? shift : '';
1350 $invoice_from = shift if scalar(@_);
1351 $notice_name = 'Invoice';
1355 $invoice_from ||= $self->_agent_invoice_from || #XXX should go away
1356 $conf->config('invoice_from', $self->cust_main->agentnum );
1358 my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ }
1359 $self->cust_main->invoicing_list;
1361 if ( ! @invoicing_list ) { #no recipients
1362 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1363 die 'No recipients for customer #'. $self->custnum;
1365 #default: better to notify this person than silence
1366 @invoicing_list = ($invoice_from);
1370 my $subject = $self->email_subject($template);
1372 my $error = send_email(
1373 $self->generate_email(
1374 'from' => $invoice_from,
1375 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1376 'subject' => $subject,
1377 'template' => $template,
1378 'notice_name' => $notice_name,
1379 'no_coupon' => $no_coupon,
1382 die "can't email invoice: $error\n" if $error;
1383 #die "$error\n" if $error;
1389 my $conf = $self->conf;
1391 #my $template = scalar(@_) ? shift : '';
1394 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1397 my $cust_main = $self->cust_main;
1398 my $name = $cust_main->name;
1399 my $name_short = $cust_main->name_short;
1400 my $invoice_number = $self->invnum;
1401 my $invoice_date = $self->_date_pretty;
1403 eval qq("$subject");
1406 =item lpr_data HASHREF | [ TEMPLATE ]
1408 Returns the postscript or plaintext for this invoice as an arrayref.
1410 Options can be passed as a hashref (recommended) or as a single optional value
1413 I<template>, if specified, is the name of a suffix for alternate invoices.
1415 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1421 my $conf = $self->conf;
1422 my( $template, $notice_name );
1425 $template = $opt->{'template'} || '';
1426 $notice_name = $opt->{'notice_name'} || 'Invoice';
1428 $template = scalar(@_) ? shift : '';
1429 $notice_name = 'Invoice';
1433 'template' => $template,
1434 'notice_name' => $notice_name,
1437 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1438 [ $self->$method( \%opt ) ];
1441 =item print HASHREF | [ TEMPLATE ]
1443 Prints this invoice.
1445 Options can be passed as a hashref (recommended) or as a single optional
1448 I<template>, if specified, is the name of a suffix for alternate invoices.
1450 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1454 #sub print_invoice {
1457 my $conf = $self->conf;
1458 my( $template, $notice_name );
1461 $template = $opt->{'template'} || '';
1462 $notice_name = $opt->{'notice_name'} || 'Invoice';
1464 $template = scalar(@_) ? shift : '';
1465 $notice_name = 'Invoice';
1469 'template' => $template,
1470 'notice_name' => $notice_name,
1473 if($conf->exists('invoice_print_pdf')) {
1474 # Add the invoice to the current batch.
1475 $self->batch_invoice(\%opt);
1478 do_print $self->lpr_data(\%opt);
1482 =item fax_invoice HASHREF | [ TEMPLATE ]
1486 Options can be passed as a hashref (recommended) or as a single optional
1489 I<template>, if specified, is the name of a suffix for alternate invoices.
1491 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1497 my $conf = $self->conf;
1498 my( $template, $notice_name );
1501 $template = $opt->{'template'} || '';
1502 $notice_name = $opt->{'notice_name'} || 'Invoice';
1504 $template = scalar(@_) ? shift : '';
1505 $notice_name = 'Invoice';
1508 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1509 unless $conf->exists('invoice_latex');
1511 my $dialstring = $self->cust_main->getfield('fax');
1515 'template' => $template,
1516 'notice_name' => $notice_name,
1519 my $error = send_fax( 'docdata' => $self->lpr_data(\%opt),
1520 'dialstring' => $dialstring,
1522 die $error if $error;
1526 =item batch_invoice [ HASHREF ]
1528 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1529 isn't an open batch, one will be created.
1534 my ($self, $opt) = @_;
1535 my $bill_batch = $self->get_open_bill_batch;
1536 my $cust_bill_batch = FS::cust_bill_batch->new({
1537 batchnum => $bill_batch->batchnum,
1538 invnum => $self->invnum,
1540 return $cust_bill_batch->insert($opt);
1543 =item get_open_batch
1545 Returns the currently open batch as an FS::bill_batch object, creating a new
1546 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1551 sub get_open_bill_batch {
1553 my $conf = $self->conf;
1554 my $hashref = { status => 'O' };
1555 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1556 ? $self->cust_main->agentnum
1558 my $batch = qsearchs('bill_batch', $hashref);
1559 return $batch if $batch;
1560 $batch = FS::bill_batch->new($hashref);
1561 my $error = $batch->insert;
1562 die $error if $error;
1566 =item ftp_invoice [ TEMPLATENAME ]
1568 Sends this invoice data via FTP.
1570 TEMPLATENAME is unused?
1576 my $conf = $self->conf;
1577 my $template = scalar(@_) ? shift : '';
1580 'protocol' => 'ftp',
1581 'server' => $conf->config('cust_bill-ftpserver'),
1582 'username' => $conf->config('cust_bill-ftpusername'),
1583 'password' => $conf->config('cust_bill-ftppassword'),
1584 'dir' => $conf->config('cust_bill-ftpdir'),
1585 'format' => $conf->config('cust_bill-ftpformat'),
1589 =item spool_invoice [ TEMPLATENAME ]
1591 Spools this invoice data (see L<FS::spool_csv>)
1593 TEMPLATENAME is unused?
1599 my $conf = $self->conf;
1600 my $template = scalar(@_) ? shift : '';
1603 'format' => $conf->config('cust_bill-spoolformat'),
1604 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1608 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1610 Like B<send>, but only sends the invoice if it is the newest open invoice for
1615 sub send_if_newest {
1620 grep { $_->owed > 0 }
1621 qsearch('cust_bill', {
1622 'custnum' => $self->custnum,
1623 #'_date' => { op=>'>', value=>$self->_date },
1624 'invnum' => { op=>'>', value=>$self->invnum },
1631 =item send_csv OPTION => VALUE, ...
1633 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1637 protocol - currently only "ftp"
1643 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1644 and YYMMDDHHMMSS is a timestamp.
1646 See L</print_csv> for a description of the output format.
1651 my($self, %opt) = @_;
1655 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1656 mkdir $spooldir, 0700 unless -d $spooldir;
1658 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1659 my $file = "$spooldir/$tracctnum.csv";
1661 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1663 open(CSV, ">$file") or die "can't open $file: $!";
1671 if ( $opt{protocol} eq 'ftp' ) {
1672 eval "use Net::FTP;";
1674 $net = Net::FTP->new($opt{server}) or die @$;
1676 die "unknown protocol: $opt{protocol}";
1679 $net->login( $opt{username}, $opt{password} )
1680 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1682 $net->binary or die "can't set binary mode";
1684 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1686 $net->put($file) or die "can't put $file: $!";
1696 Spools CSV invoice data.
1702 =item format - 'default' or 'billco'
1704 =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>).
1706 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1708 =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.
1715 my($self, %opt) = @_;
1717 my $cust_main = $self->cust_main;
1719 if ( $opt{'dest'} ) {
1720 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1721 $cust_main->invoicing_list;
1722 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1723 || ! keys %invoicing_list;
1726 if ( $opt{'balanceover'} ) {
1728 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1731 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1732 mkdir $spooldir, 0700 unless -d $spooldir;
1734 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1738 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1739 ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1742 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1744 open(CSV, ">>$file") or die "can't open $file: $!";
1745 flock(CSV, LOCK_EX);
1750 if ( lc($opt{'format'}) eq 'billco' ) {
1752 flock(CSV, LOCK_UN);
1757 ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1760 open(CSV,">>$file") or die "can't open $file: $!";
1761 flock(CSV, LOCK_EX);
1767 flock(CSV, LOCK_UN);
1774 =item print_csv OPTION => VALUE, ...
1776 Returns CSV data for this invoice.
1780 format - 'default' or 'billco'
1782 Returns a list consisting of two scalars. The first is a single line of CSV
1783 header information for this invoice. The second is one or more lines of CSV
1784 detail information for this invoice.
1786 If I<format> is not specified or "default", the fields of the CSV file are as
1789 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1793 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1795 B<record_type> is C<cust_bill> for the initial header line only. The
1796 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1797 fields are filled in.
1799 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1800 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1803 =item invnum - invoice number
1805 =item custnum - customer number
1807 =item _date - invoice date
1809 =item charged - total invoice amount
1811 =item first - customer first name
1813 =item last - customer first name
1815 =item company - company name
1817 =item address1 - address line 1
1819 =item address2 - address line 1
1829 =item pkg - line item description
1831 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1833 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1835 =item sdate - start date for recurring fee
1837 =item edate - end date for recurring fee
1841 If I<format> is "billco", the fields of the header CSV file are as follows:
1843 +-------------------------------------------------------------------+
1844 | FORMAT HEADER FILE |
1845 |-------------------------------------------------------------------|
1846 | Field | Description | Name | Type | Width |
1847 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1848 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1849 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1850 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1851 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1852 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1853 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1854 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1855 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1856 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1857 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1858 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1859 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1860 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1861 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1862 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1863 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1864 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1865 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1866 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1867 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1868 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1869 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1870 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1871 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1872 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1873 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1874 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1875 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1876 +-------+-------------------------------+------------+------+-------+
1878 If I<format> is "billco", the fields of the detail CSV file are as follows:
1880 FORMAT FOR DETAIL FILE
1882 Field | Description | Name | Type | Width
1883 1 | N/A-Leave Empty | RC | CHAR | 2
1884 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1885 3 | Account Number | TRACCTNUM | CHAR | 15
1886 4 | Invoice Number | TRINVOICE | CHAR | 15
1887 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1888 6 | Transaction Detail | DETAILS | CHAR | 100
1889 7 | Amount | AMT | NUM* | 9
1890 8 | Line Format Control** | LNCTRL | CHAR | 2
1891 9 | Grouping Code | GROUP | CHAR | 2
1892 10 | User Defined | ACCT CODE | CHAR | 15
1897 my($self, %opt) = @_;
1899 eval "use Text::CSV_XS";
1902 my $cust_main = $self->cust_main;
1904 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1906 if ( lc($opt{'format'}) eq 'billco' ) {
1909 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1911 my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1913 my( $previous_balance, @unused ) = $self->previous; #previous balance
1915 my $pmt_cr_applied = 0;
1916 $pmt_cr_applied += $_->{'amount'}
1917 foreach ( $self->_items_payments, $self->_items_credits ) ;
1919 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1922 '', # 1 | N/A-Leave Empty CHAR 2
1923 '', # 2 | N/A-Leave Empty CHAR 15
1924 $opt{'tracctnum'}, # 3 | Transaction Account No CHAR 15
1925 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1926 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1927 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1928 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1929 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1930 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1931 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1932 '', # 10 | Ancillary Billing Information CHAR 30
1933 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1934 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1937 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1940 $duedate, # 14 | Bill Due Date CHAR 10
1942 $previous_balance, # 15 | Previous Balance NUM* 9
1943 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1944 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1945 $totaldue, # 18 | Total Amt Due NUM* 9
1946 $totaldue, # 19 | Total Amt Due NUM* 9
1947 '', # 20 | 30 Day Aging NUM* 9
1948 '', # 21 | 60 Day Aging NUM* 9
1949 '', # 22 | 90 Day Aging NUM* 9
1950 'N', # 23 | Y/N CHAR 1
1951 '', # 24 | Remittance automation CHAR 100
1952 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1953 $self->custnum, # 26 | Customer Reference Number CHAR 15
1954 '0', # 27 | Federal Tax*** NUM* 9
1955 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1956 '0', # 29 | Other Taxes & Fees*** NUM* 9
1965 time2str("%x", $self->_date),
1966 sprintf("%.2f", $self->charged),
1967 ( map { $cust_main->getfield($_) }
1968 qw( first last company address1 address2 city state zip country ) ),
1970 ) or die "can't create csv";
1973 my $header = $csv->string. "\n";
1976 if ( lc($opt{'format'}) eq 'billco' ) {
1979 foreach my $item ( $self->_items_pkg ) {
1982 '', # 1 | N/A-Leave Empty CHAR 2
1983 '', # 2 | N/A-Leave Empty CHAR 15
1984 $opt{'tracctnum'}, # 3 | Account Number CHAR 15
1985 $self->invnum, # 4 | Invoice Number CHAR 15
1986 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1987 $item->{'description'}, # 6 | Transaction Detail CHAR 100
1988 $item->{'amount'}, # 7 | Amount NUM* 9
1989 '', # 8 | Line Format Control** CHAR 2
1990 '', # 9 | Grouping Code CHAR 2
1991 '', # 10 | User Defined CHAR 15
1994 $detail .= $csv->string. "\n";
2000 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2002 my($pkg, $setup, $recur, $sdate, $edate);
2003 if ( $cust_bill_pkg->pkgnum ) {
2005 ($pkg, $setup, $recur, $sdate, $edate) = (
2006 $cust_bill_pkg->part_pkg->pkg,
2007 ( $cust_bill_pkg->setup != 0
2008 ? sprintf("%.2f", $cust_bill_pkg->setup )
2010 ( $cust_bill_pkg->recur != 0
2011 ? sprintf("%.2f", $cust_bill_pkg->recur )
2013 ( $cust_bill_pkg->sdate
2014 ? time2str("%x", $cust_bill_pkg->sdate)
2016 ($cust_bill_pkg->edate
2017 ?time2str("%x", $cust_bill_pkg->edate)
2021 } else { #pkgnum tax
2022 next unless $cust_bill_pkg->setup != 0;
2023 $pkg = $cust_bill_pkg->desc;
2024 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2025 ( $sdate, $edate ) = ( '', '' );
2031 ( map { '' } (1..11) ),
2032 ($pkg, $setup, $recur, $sdate, $edate)
2033 ) or die "can't create csv";
2035 $detail .= $csv->string. "\n";
2041 ( $header, $detail );
2047 Pays this invoice with a compliemntary payment. If there is an error,
2048 returns the error, otherwise returns false.
2054 my $cust_pay = new FS::cust_pay ( {
2055 'invnum' => $self->invnum,
2056 'paid' => $self->owed,
2059 'payinfo' => $self->cust_main->payinfo,
2067 Attempts to pay this invoice with a credit card payment via a
2068 Business::OnlinePayment realtime gateway. See
2069 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2070 for supported processors.
2076 $self->realtime_bop( 'CC', @_ );
2081 Attempts to pay this invoice with an electronic check (ACH) payment via a
2082 Business::OnlinePayment realtime gateway. See
2083 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2084 for supported processors.
2090 $self->realtime_bop( 'ECHECK', @_ );
2095 Attempts to pay this invoice with phone bill (LEC) payment via a
2096 Business::OnlinePayment realtime gateway. See
2097 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2098 for supported processors.
2104 $self->realtime_bop( 'LEC', @_ );
2108 my( $self, $method ) = (shift,shift);
2109 my $conf = $self->conf;
2112 my $cust_main = $self->cust_main;
2113 my $balance = $cust_main->balance;
2114 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2115 $amount = sprintf("%.2f", $amount);
2116 return "not run (balance $balance)" unless $amount > 0;
2118 my $description = 'Internet Services';
2119 if ( $conf->exists('business-onlinepayment-description') ) {
2120 my $dtempl = $conf->config('business-onlinepayment-description');
2122 my $agent_obj = $cust_main->agent
2123 or die "can't retreive agent for $cust_main (agentnum ".
2124 $cust_main->agentnum. ")";
2125 my $agent = $agent_obj->agent;
2126 my $pkgs = join(', ',
2127 map { $_->part_pkg->pkg }
2128 grep { $_->pkgnum } $self->cust_bill_pkg
2130 $description = eval qq("$dtempl");
2133 $cust_main->realtime_bop($method, $amount,
2134 'description' => $description,
2135 'invnum' => $self->invnum,
2136 #this didn't do what we want, it just calls apply_payments_and_credits
2138 'apply_to_invoice' => 1,
2141 #this changes application behavior: auto payments
2142 #triggered against a specific invoice are now applied
2143 #to that invoice instead of oldest open.
2149 =item batch_card OPTION => VALUE...
2151 Adds a payment for this invoice to the pending credit card batch (see
2152 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2153 runs the payment using a realtime gateway.
2158 my ($self, %options) = @_;
2159 my $cust_main = $self->cust_main;
2161 $options{invnum} = $self->invnum;
2163 $cust_main->batch_card(%options);
2166 sub _agent_template {
2168 $self->cust_main->agent_template;
2171 sub _agent_invoice_from {
2173 $self->cust_main->agent_invoice_from;
2176 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2178 Returns an text invoice, as a list of lines.
2180 Options can be passed as a hashref (recommended) or as a list of time, template
2181 and then any key/value pairs for any other options.
2183 I<time>, if specified, is used to control the printing of overdue messages. The
2184 default is now. It isn't the date of the invoice; that's the `_date' field.
2185 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2186 L<Time::Local> and L<Date::Parse> for conversion functions.
2188 I<template>, if specified, is the name of a suffix for alternate invoices.
2190 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2196 my( $today, $template, %opt );
2198 %opt = %{ shift() };
2199 $today = delete($opt{'time'}) || '';
2200 $template = delete($opt{template}) || '';
2202 ( $today, $template, %opt ) = @_;
2205 my %params = ( 'format' => 'template' );
2206 $params{'time'} = $today if $today;
2207 $params{'template'} = $template if $template;
2208 $params{$_} = $opt{$_}
2209 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2211 $self->print_generic( %params );
2214 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2216 Internal method - returns a filename of a filled-in LaTeX template for this
2217 invoice (Note: add ".tex" to get the actual filename), and a filename of
2218 an associated logo (with the .eps extension included).
2220 See print_ps and print_pdf for methods that return PostScript and PDF output.
2222 Options can be passed as a hashref (recommended) or as a list of time, template
2223 and then any key/value pairs for any other options.
2225 I<time>, if specified, is used to control the printing of overdue messages. The
2226 default is now. It isn't the date of the invoice; that's the `_date' field.
2227 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2228 L<Time::Local> and L<Date::Parse> for conversion functions.
2230 I<template>, if specified, is the name of a suffix for alternate invoices.
2232 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2238 my $conf = $self->conf;
2239 my( $today, $template, %opt );
2241 %opt = %{ shift() };
2242 $today = delete($opt{'time'}) || '';
2243 $template = delete($opt{template}) || '';
2245 ( $today, $template, %opt ) = @_;
2248 my %params = ( 'format' => 'latex' );
2249 $params{'time'} = $today if $today;
2250 $params{'template'} = $template if $template;
2251 $params{$_} = $opt{$_}
2252 foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
2254 $template ||= $self->_agent_template;
2256 my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2257 my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2261 ) or die "can't open temp file: $!\n";
2263 my $agentnum = $self->cust_main->agentnum;
2265 if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2266 print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2267 or die "can't write temp file: $!\n";
2269 print $lh $conf->config_binary('logo.eps', $agentnum)
2270 or die "can't write temp file: $!\n";
2273 $params{'logo_file'} = $lh->filename;
2275 if($conf->exists('invoice-barcode')){
2276 my $png_file = $self->invoice_barcode($dir);
2277 my $eps_file = $png_file;
2278 $eps_file =~ s/\.png$/.eps/g;
2279 $png_file =~ /(barcode.*png)/;
2281 $eps_file =~ /(barcode.*eps)/;
2284 my $curr_dir = cwd();
2286 # after painfuly long experimentation, it was determined that sam2p won't
2287 # accept : and other chars in the path, no matter how hard I tried to
2288 # escape them, hence the chdir (and chdir back, just to be safe)
2289 system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
2290 or die "sam2p failed: $!\n";
2294 $params{'barcode_file'} = $eps_file;
2297 my @filled_in = $self->print_generic( %params );
2299 my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2303 ) or die "can't open temp file: $!\n";
2304 binmode($fh, ':utf8'); # language support
2305 print $fh join('', @filled_in );
2308 $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2309 return ($1, $params{'logo_file'}, $params{'barcode_file'});
2313 =item invoice_barcode DIR_OR_FALSE
2315 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2316 it is taken as the temp directory where the PNG file will be generated and the
2317 PNG file name is returned. Otherwise, the PNG image itself is returned.
2321 sub invoice_barcode {
2322 my ($self, $dir) = (shift,shift);
2324 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2325 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2326 my $gd = $gdbar->plot(Height => 30);
2329 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2333 ) or die "can't open temp file: $!\n";
2334 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2335 my $png_file = $bh->filename;
2342 =item print_generic OPTION => VALUE ...
2344 Internal method - returns a filled-in template for this invoice as a scalar.
2346 See print_ps and print_pdf for methods that return PostScript and PDF output.
2348 Non optional options include
2349 format - latex, html, template
2351 Optional options include
2353 template - a value used as a suffix for a configuration template
2355 time - a value used to control the printing of overdue messages. The
2356 default is now. It isn't the date of the invoice; that's the `_date' field.
2357 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
2358 L<Time::Local> and L<Date::Parse> for conversion functions.
2362 unsquelch_cdr - overrides any per customer cdr squelching when true
2364 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2368 #what's with all the sprintf('%10.2f')'s in here? will it cause any
2369 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2370 # yes: fixed width (dot matrix) text printing will be borked
2372 my( $self, %params ) = @_;
2373 my $conf = $self->conf;
2374 my $today = $params{today} ? $params{today} : time;
2375 warn "$me print_generic called on $self with suffix $params{template}\n"
2378 my $format = $params{format};
2379 die "Unknown format: $format"
2380 unless $format =~ /^(latex|html|template)$/;
2382 my $cust_main = $self->cust_main;
2383 $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2384 unless $cust_main->payname
2385 && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2387 my %delimiters = ( 'latex' => [ '[@--', '--@]' ],
2388 'html' => [ '<%=', '%>' ],
2389 'template' => [ '{', '}' ],
2392 warn "$me print_generic creating template\n"
2395 #create the template
2396 my $template = $params{template} ? $params{template} : $self->_agent_template;
2397 my $templatefile = "invoice_$format";
2398 $templatefile .= "_$template"
2399 if length($template) && $conf->exists($templatefile."_$template");
2400 my @invoice_template = map "$_\n", $conf->config($templatefile)
2401 or die "cannot load config data $templatefile";
2404 if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2405 #change this to a die when the old code is removed
2406 warn "old-style invoice template $templatefile; ".
2407 "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2408 $old_latex = 'true';
2409 @invoice_template = _translate_old_latex_format(@invoice_template);
2412 warn "$me print_generic creating T:T object\n"
2415 my $text_template = new Text::Template(
2417 SOURCE => \@invoice_template,
2418 DELIMITERS => $delimiters{$format},
2421 warn "$me print_generic compiling T:T object\n"
2424 $text_template->compile()
2425 or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2428 # additional substitution could possibly cause breakage in existing templates
2429 my %convert_maps = (
2431 'notes' => sub { map "$_", @_ },
2432 'footer' => sub { map "$_", @_ },
2433 'smallfooter' => sub { map "$_", @_ },
2434 'returnaddress' => sub { map "$_", @_ },
2435 'coupon' => sub { map "$_", @_ },
2436 'summary' => sub { map "$_", @_ },
2442 s/%%(.*)$/<!-- $1 -->/g;
2443 s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2444 s/\\begin\{enumerate\}/<ol>/g;
2446 s/\\end\{enumerate\}/<\/ol>/g;
2447 s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2456 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2458 sub { map { s/~/ /g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2463 s/\\\\\*?\s*$/<BR>/;
2464 s/\\hyphenation\{[\w\s\-]+}//;
2469 'coupon' => sub { "" },
2470 'summary' => sub { "" },
2477 s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2478 s/\\begin\{enumerate\}//g;
2480 s/\\end\{enumerate\}//g;
2481 s/\\textbf\{(.*)\}/$1/g;
2488 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2490 sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2495 s/\\\\\*?\s*$/\n/; # dubious
2496 s/\\hyphenation\{[\w\s\-]+}//;
2500 'coupon' => sub { "" },
2501 'summary' => sub { "" },
2506 # hashes for differing output formats
2507 my %nbsps = ( 'latex' => '~',
2508 'html' => '', # '&nbps;' would be nice
2509 'template' => '', # not used
2511 my $nbsp = $nbsps{$format};
2513 my %escape_functions = ( 'latex' => \&_latex_escape,
2514 'html' => \&_html_escape_nbsp,#\&encode_entities,
2515 'template' => sub { shift },
2517 my $escape_function = $escape_functions{$format};
2518 my $escape_function_nonbsp = ($format eq 'html')
2519 ? \&_html_escape : $escape_function;
2521 my %date_formats = ( 'latex' => $date_format_long,
2522 'html' => $date_format_long,
2525 $date_formats{'html'} =~ s/ / /g;
2527 my $date_format = $date_formats{$format};
2529 my %embolden_functions = ( 'latex' => sub { return '\textbf{'. shift(). '}'
2531 'html' => sub { return '<b>'. shift(). '</b>'
2533 'template' => sub { shift },
2535 my $embolden_function = $embolden_functions{$format};
2537 my %newline_tokens = ( 'latex' => '\\\\',
2541 my $newline_token = $newline_tokens{$format};
2543 warn "$me generating template variables\n"
2546 # generate template variables
2549 defined( $conf->config_orbase( "invoice_${format}returnaddress",
2553 && length( $conf->config_orbase( "invoice_${format}returnaddress",
2559 $returnaddress = join("\n",
2560 $conf->config_orbase("invoice_${format}returnaddress", $template)
2563 } elsif ( grep /\S/,
2564 $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2566 my $convert_map = $convert_maps{$format}{'returnaddress'};
2569 &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2574 } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2576 my $convert_map = $convert_maps{$format}{'returnaddress'};
2577 $returnaddress = join( "\n", &$convert_map(
2578 map { s/( {2,})/'~' x length($1)/eg;
2582 ( $conf->config('company_name', $self->cust_main->agentnum),
2583 $conf->config('company_address', $self->cust_main->agentnum),
2590 my $warning = "Couldn't find a return address; ".
2591 "do you need to set the company_address configuration value?";
2593 $returnaddress = $nbsp;
2594 #$returnaddress = $warning;
2598 warn "$me generating invoice data\n"
2601 my $agentnum = $self->cust_main->agentnum;
2603 my %invoice_data = (
2606 'company_name' => scalar( $conf->config('company_name', $agentnum) ),
2607 'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2608 'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
2609 'returnaddress' => $returnaddress,
2610 'agent' => &$escape_function($cust_main->agent->agent),
2613 'invnum' => $self->invnum,
2614 'date' => time2str($date_format, $self->_date),
2615 'today' => time2str($date_format_long, $today),
2616 'terms' => $self->terms,
2617 'template' => $template, #params{'template'},
2618 'notice_name' => ($params{'notice_name'} || 'Invoice'),#escape_function?
2619 'current_charges' => sprintf("%.2f", $self->charged),
2620 'duedate' => $self->due_date2str($rdate_format), #date_format?
2623 'custnum' => $cust_main->display_custnum,
2624 'agent_custid' => &$escape_function($cust_main->agent_custid),
2625 ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2626 payname company address1 address2 city state zip fax
2630 'ship_enable' => $conf->exists('invoice-ship_address'),
2631 'unitprices' => $conf->exists('invoice-unitprice'),
2632 'smallernotes' => $conf->exists('invoice-smallernotes'),
2633 'smallerfooter' => $conf->exists('invoice-smallerfooter'),
2634 'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2636 #layout info -- would be fancy to calc some of this and bury the template
2638 'topmargin' => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2639 'headsep' => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2640 'textheight' => scalar($conf->config('invoice_latextextheight', $agentnum)),
2641 'extracouponspace' => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2642 'couponfootsep' => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2643 'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2644 'addresssep' => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2645 'amountenclosedsep' => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2646 'coupontoaddresssep' => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2647 'addcompanytoaddress' => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2649 # better hang on to conf_dir for a while (for old templates)
2650 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2652 #these are only used when doing paged plaintext
2659 my $lh = FS::L10N->get_handle($cust_main->locale);
2660 $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
2661 my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
2662 # eval to avoid death for unimplemented languages
2663 my $dh = eval { Date::Language->new($info{'name'}) } ||
2664 Date::Language->new(); # fall back to English
2665 $invoice_data{'time2str'} = sub { $dh->time2str(@_) };
2666 # eventually use this date handle everywhere in here, too
2668 my $min_sdate = 999999999999;
2670 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2671 next unless $cust_bill_pkg->pkgnum > 0;
2672 $min_sdate = $cust_bill_pkg->sdate
2673 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2674 $max_edate = $cust_bill_pkg->edate
2675 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2678 $invoice_data{'bill_period'} = '';
2679 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2680 . " to " . time2str('%e %h', $max_edate)
2681 if ($max_edate != 0 && $min_sdate != 999999999999);
2683 $invoice_data{finance_section} = '';
2684 if ( $conf->config('finance_pkgclass') ) {
2686 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2687 $invoice_data{finance_section} = $pkg_class->categoryname;
2689 $invoice_data{finance_amount} = '0.00';
2690 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2692 my $countrydefault = $conf->config('countrydefault') || 'US';
2693 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2694 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2695 my $method = $prefix.$_;
2696 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2698 $invoice_data{'ship_country'} = ''
2699 if ( $invoice_data{'ship_country'} eq $countrydefault );
2701 $invoice_data{'cid'} = $params{'cid'}
2704 if ( $cust_main->country eq $countrydefault ) {
2705 $invoice_data{'country'} = '';
2707 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2711 $invoice_data{'address'} = \@address;
2713 $cust_main->payname.
2714 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2715 ? " (P.O. #". $cust_main->payinfo. ")"
2719 push @address, $cust_main->company
2720 if $cust_main->company;
2721 push @address, $cust_main->address1;
2722 push @address, $cust_main->address2
2723 if $cust_main->address2;
2725 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2726 push @address, $invoice_data{'country'}
2727 if $invoice_data{'country'};
2729 while (scalar(@address) < 5);
2731 $invoice_data{'logo_file'} = $params{'logo_file'}
2732 if $params{'logo_file'};
2733 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2734 if $params{'barcode_file'};
2735 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2736 if $params{'barcode_img'};
2737 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2738 if $params{'barcode_cid'};
2740 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2741 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2742 #my $balance_due = $self->owed + $pr_total - $cr_total;
2743 my $balance_due = $self->owed + $pr_total;
2745 # the customer's current balance as shown on the invoice before this one
2746 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2748 # the change in balance from that invoice to this one
2749 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2751 # the sum of amount owed on all previous invoices
2752 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2754 # the sum of amount owed on all invoices
2755 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2757 # info from customer's last invoice before this one, for some
2759 $invoice_data{'last_bill'} = {};
2760 my $last_bill = $pr_cust_bill[-1];
2762 $invoice_data{'last_bill'} = {
2763 '_date' => $last_bill->_date, #unformatted
2764 # all we need for now
2768 my $summarypage = '';
2769 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2772 $invoice_data{'summarypage'} = $summarypage;
2774 warn "$me substituting variables in notes, footer, smallfooter\n"
2777 my @include = (qw( notes footer smallfooter ));
2778 push @include, 'coupon' unless $params{'no_coupon'};
2779 foreach my $include (@include) {
2781 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2784 if ( $conf->exists($inc_file, $agentnum)
2785 && length( $conf->config($inc_file, $agentnum) ) ) {
2787 @inc_src = $conf->config($inc_file, $agentnum);
2791 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2793 my $convert_map = $convert_maps{$format}{$include};
2795 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2796 s/--\@\]/$delimiters{$format}[1]/g;
2799 &$convert_map( $conf->config($inc_file, $agentnum) );
2803 my $inc_tt = new Text::Template (
2805 SOURCE => [ map "$_\n", @inc_src ],
2806 DELIMITERS => $delimiters{$format},
2807 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2809 unless ( $inc_tt->compile() ) {
2810 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2811 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2815 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2817 $invoice_data{$include} =~ s/\n+$//
2818 if ($format eq 'latex');
2821 # let invoices use either of these as needed
2822 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2823 ? $cust_main->payinfo : '';
2824 $invoice_data{'po_line'} =
2825 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2826 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2829 my %money_chars = ( 'latex' => '',
2830 'html' => $conf->config('money_char') || '$',
2833 my $money_char = $money_chars{$format};
2835 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2836 'html' => $conf->config('money_char') || '$',
2839 my $other_money_char = $other_money_chars{$format};
2840 $invoice_data{'dollar'} = $other_money_char;
2842 my @detail_items = ();
2843 my @total_items = ();
2847 $invoice_data{'detail_items'} = \@detail_items;
2848 $invoice_data{'total_items'} = \@total_items;
2849 $invoice_data{'buf'} = \@buf;
2850 $invoice_data{'sections'} = \@sections;
2852 warn "$me generating sections\n"
2855 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2856 'subtotal' => $other_money_char.
2857 sprintf('%.2f', $pr_total),
2858 'summarized' => $summarypage ? 'Y' : '',
2860 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2861 join(' / ', map { $cust_main->balance_date_range(@$_) }
2862 $self->_prior_month30s
2864 if $conf->exists('invoice_include_aging');
2867 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2868 'subtotal' => $taxtotal, # adjusted below
2869 'summarized' => $summarypage ? 'Y' : '',
2871 my $tax_weight = _pkg_category($tax_section->{description})
2872 ? _pkg_category($tax_section->{description})->weight
2874 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2875 $tax_section->{'sort_weight'} = $tax_weight;
2878 my $adjusttotal = 0;
2879 my $adjust_section = { 'description' =>
2880 $self->mt('Credits, Payments, and Adjustments'),
2881 'subtotal' => 0, # adjusted below
2882 'summarized' => $summarypage ? 'Y' : '',
2884 my $adjust_weight = _pkg_category($adjust_section->{description})
2885 ? _pkg_category($adjust_section->{description})->weight
2887 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2888 $adjust_section->{'sort_weight'} = $adjust_weight;
2890 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2891 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2892 $invoice_data{'multisection'} = $multisection;
2893 my $late_sections = [];
2894 my $extra_sections = [];
2895 my $extra_lines = ();
2896 if ( $multisection ) {
2897 ($extra_sections, $extra_lines) =
2898 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2899 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2901 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2903 push @detail_items, @$extra_lines if $extra_lines;
2905 $self->_items_sections( $late_sections, # this could stand a refactor
2907 $escape_function_nonbsp,
2911 if ($conf->exists('svc_phone_sections')) {
2912 my ($phone_sections, $phone_lines) =
2913 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2914 push @{$late_sections}, @$phone_sections;
2915 push @detail_items, @$phone_lines;
2917 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2918 my ($accountcode_section, $accountcode_lines) =
2919 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2920 if ( scalar(@$accountcode_lines) ) {
2921 push @{$late_sections}, $accountcode_section;
2922 push @detail_items, @$accountcode_lines;
2925 } else {# not multisection
2926 # make a default section
2927 push @sections, { 'description' => '', 'subtotal' => '' };
2928 # and calculate the finance charge total, since it won't get done otherwise.
2929 # XXX possibly other totals?
2930 # XXX possibly finance_pkgclass should not be used in this manner?
2931 if ( $conf->exists('finance_pkgclass') ) {
2932 my @finance_charges;
2933 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2934 if ( grep { $_->section eq $invoice_data{finance_section} }
2935 $cust_bill_pkg->cust_bill_pkg_display ) {
2936 # I think these are always setup fees, but just to be sure...
2937 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2940 $invoice_data{finance_amount} =
2941 sprintf('%.2f', sum( @finance_charges ) || 0);
2945 unless ( $conf->exists('disable_previous_balance')
2946 || $conf->exists('previous_balance-summary_only')
2950 warn "$me adding previous balances\n"
2953 foreach my $line_item ( $self->_items_previous ) {
2956 ext_description => [],
2958 $detail->{'ref'} = $line_item->{'pkgnum'};
2959 $detail->{'quantity'} = 1;
2960 $detail->{'section'} = $previous_section;
2961 $detail->{'description'} = &$escape_function($line_item->{'description'});
2962 if ( exists $line_item->{'ext_description'} ) {
2963 @{$detail->{'ext_description'}} = map {
2964 &$escape_function($_);
2965 } @{$line_item->{'ext_description'}};
2967 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2968 $line_item->{'amount'};
2969 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2971 push @detail_items, $detail;
2972 push @buf, [ $detail->{'description'},
2973 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2979 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2980 push @buf, ['','-----------'];
2981 push @buf, [ $self->mt('Total Previous Balance'),
2982 $money_char. sprintf("%10.2f", $pr_total) ];
2986 if ( $conf->exists('svc_phone-did-summary') ) {
2987 warn "$me adding DID summary\n"
2990 my ($didsummary,$minutes) = $self->_did_summary;
2991 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2993 { 'description' => $didsummary_desc,
2994 'ext_description' => [ $didsummary, $minutes ],
2998 foreach my $section (@sections, @$late_sections) {
3000 warn "$me adding section \n". Dumper($section)
3003 # begin some normalization
3004 $section->{'subtotal'} = $section->{'amount'}
3006 && !exists($section->{subtotal})
3007 && exists($section->{amount});
3009 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3010 if ( $invoice_data{finance_section} &&
3011 $section->{'description'} eq $invoice_data{finance_section} );
3013 $section->{'subtotal'} = $other_money_char.
3014 sprintf('%.2f', $section->{'subtotal'})
3017 # continue some normalization
3018 $section->{'amount'} = $section->{'subtotal'}
3022 if ( $section->{'description'} ) {
3023 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3028 warn "$me setting options\n"
3031 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3033 $options{'section'} = $section if $multisection;
3034 $options{'format'} = $format;
3035 $options{'escape_function'} = $escape_function;
3036 $options{'format_function'} = sub { () } unless $unsquelched;
3037 $options{'unsquelched'} = $unsquelched;
3038 $options{'summary_page'} = $summarypage;
3039 $options{'skip_usage'} =
3040 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3041 $options{'multilocation'} = $multilocation;
3042 $options{'multisection'} = $multisection;
3044 warn "$me searching for line items\n"
3047 foreach my $line_item ( $self->_items_pkg(%options) ) {
3049 warn "$me adding line item $line_item\n"
3053 ext_description => [],
3055 $detail->{'ref'} = $line_item->{'pkgnum'};
3056 $detail->{'quantity'} = $line_item->{'quantity'};
3057 $detail->{'section'} = $section;
3058 $detail->{'description'} = &$escape_function($line_item->{'description'});
3059 if ( exists $line_item->{'ext_description'} ) {
3060 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3062 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3063 $line_item->{'amount'};
3064 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3065 $line_item->{'unit_amount'};
3066 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3068 $detail->{'sdate'} = $line_item->{'sdate'};
3069 $detail->{'edate'} = $line_item->{'edate'};
3070 $detail->{'seconds'} = $line_item->{'seconds'};
3072 push @detail_items, $detail;
3073 push @buf, ( [ $detail->{'description'},
3074 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3076 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3080 if ( $section->{'description'} ) {
3081 push @buf, ( ['','-----------'],
3082 [ $section->{'description'}. ' sub-total',
3083 $section->{'subtotal'} # already formatted this
3092 $invoice_data{current_less_finance} =
3093 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3095 if ( $multisection && !$conf->exists('disable_previous_balance')
3096 || $conf->exists('previous_balance-summary_only') )
3098 unshift @sections, $previous_section if $pr_total;
3101 warn "$me adding taxes\n"
3104 foreach my $tax ( $self->_items_tax ) {
3106 $taxtotal += $tax->{'amount'};
3108 my $description = &$escape_function( $tax->{'description'} );
3109 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3111 if ( $multisection ) {
3113 my $money = $old_latex ? '' : $money_char;
3114 push @detail_items, {
3115 ext_description => [],
3118 description => $description,
3119 amount => $money. $amount,
3121 section => $tax_section,
3126 push @total_items, {
3127 'total_item' => $description,
3128 'total_amount' => $other_money_char. $amount,
3133 push @buf,[ $description,
3134 $money_char. $amount,
3141 $total->{'total_item'} = $self->mt('Sub-total');
3142 $total->{'total_amount'} =
3143 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3145 if ( $multisection ) {
3146 $tax_section->{'subtotal'} = $other_money_char.
3147 sprintf('%.2f', $taxtotal);
3148 $tax_section->{'pretotal'} = 'New charges sub-total '.
3149 $total->{'total_amount'};
3150 push @sections, $tax_section if $taxtotal;
3152 unshift @total_items, $total;
3155 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3157 push @buf,['','-----------'];
3158 push @buf,[$self->mt(
3159 $conf->exists('disable_previous_balance')
3161 : 'Total New Charges'
3163 $money_char. sprintf("%10.2f",$self->charged) ];
3169 $item = $conf->config('previous_balance-exclude_from_total')
3170 || 'Total New Charges'
3171 if $conf->exists('previous_balance-exclude_from_total');
3172 my $amount = $self->charged +
3173 ( $conf->exists('disable_previous_balance') ||
3174 $conf->exists('previous_balance-exclude_from_total')
3178 $total->{'total_item'} = &$embolden_function($self->mt($item));
3179 $total->{'total_amount'} =
3180 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3181 if ( $multisection ) {
3182 if ( $adjust_section->{'sort_weight'} ) {
3183 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3184 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3186 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3187 $other_money_char. sprintf('%.2f', $self->charged );
3190 push @total_items, $total;
3192 push @buf,['','-----------'];
3195 sprintf( '%10.2f', $amount )
3200 unless ( $conf->exists('disable_previous_balance') ) {
3201 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3204 my $credittotal = 0;
3205 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3208 $total->{'total_item'} = &$escape_function($credit->{'description'});
3209 $credittotal += $credit->{'amount'};
3210 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3211 $adjusttotal += $credit->{'amount'};
3212 if ( $multisection ) {
3213 my $money = $old_latex ? '' : $money_char;
3214 push @detail_items, {
3215 ext_description => [],
3218 description => &$escape_function($credit->{'description'}),
3219 amount => $money. $credit->{'amount'},
3221 section => $adjust_section,
3224 push @total_items, $total;
3228 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3231 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3232 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3236 my $paymenttotal = 0;
3237 foreach my $payment ( $self->_items_payments ) {
3239 $total->{'total_item'} = &$escape_function($payment->{'description'});
3240 $paymenttotal += $payment->{'amount'};
3241 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3242 $adjusttotal += $payment->{'amount'};
3243 if ( $multisection ) {
3244 my $money = $old_latex ? '' : $money_char;
3245 push @detail_items, {
3246 ext_description => [],
3249 description => &$escape_function($payment->{'description'}),
3250 amount => $money. $payment->{'amount'},
3252 section => $adjust_section,
3255 push @total_items, $total;
3257 push @buf, [ $payment->{'description'},
3258 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3261 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3263 if ( $multisection ) {
3264 $adjust_section->{'subtotal'} = $other_money_char.
3265 sprintf('%.2f', $adjusttotal);
3266 push @sections, $adjust_section
3267 unless $adjust_section->{sort_weight};
3272 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3273 $total->{'total_amount'} =
3274 &$embolden_function(
3275 $other_money_char. sprintf('%.2f', $summarypage
3277 $self->billing_balance
3278 : $self->owed + $pr_total
3281 if ( $multisection && !$adjust_section->{sort_weight} ) {
3282 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3283 $total->{'total_amount'};
3285 push @total_items, $total;
3287 push @buf,['','-----------'];
3288 push @buf,[$self->balance_due_msg, $money_char.
3289 sprintf("%10.2f", $balance_due ) ];
3292 if ( $conf->exists('previous_balance-show_credit')
3293 and $cust_main->balance < 0 ) {
3294 my $credit_total = {
3295 'total_item' => &$embolden_function($self->credit_balance_msg),
3296 'total_amount' => &$embolden_function(
3297 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3300 if ( $multisection ) {
3301 $adjust_section->{'posttotal'} .= $newline_token .
3302 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3305 push @total_items, $credit_total;
3307 push @buf,['','-----------'];
3308 push @buf,[$self->credit_balance_msg, $money_char.
3309 sprintf("%10.2f", -$cust_main->balance ) ];
3313 if ( $multisection ) {
3314 if ($conf->exists('svc_phone_sections')) {
3316 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3317 $total->{'total_amount'} =
3318 &$embolden_function(
3319 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3321 my $last_section = pop @sections;
3322 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3323 $total->{'total_amount'};
3324 push @sections, $last_section;
3326 push @sections, @$late_sections
3330 my @includelist = ();
3331 push @includelist, 'summary' if $summarypage;
3332 foreach my $include ( @includelist ) {
3334 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3337 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3339 @inc_src = $conf->config($inc_file, $agentnum);
3343 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3345 my $convert_map = $convert_maps{$format}{$include};
3347 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3348 s/--\@\]/$delimiters{$format}[1]/g;
3351 &$convert_map( $conf->config($inc_file, $agentnum) );
3355 my $inc_tt = new Text::Template (
3357 SOURCE => [ map "$_\n", @inc_src ],
3358 DELIMITERS => $delimiters{$format},
3359 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3361 unless ( $inc_tt->compile() ) {
3362 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3363 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3367 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3369 $invoice_data{$include} =~ s/\n+$//
3370 if ($format eq 'latex');
3375 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3376 /invoice_lines\((\d*)\)/;
3377 $invoice_lines += $1 || scalar(@buf);
3380 die "no invoice_lines() functions in template?"
3381 if ( $format eq 'template' && !$wasfunc );
3383 if ($format eq 'template') {
3385 if ( $invoice_lines ) {
3386 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3387 $invoice_data{'total_pages'}++
3388 if scalar(@buf) % $invoice_lines;
3391 #setup subroutine for the template
3392 #sub FS::cust_bill::_template::invoice_lines { # good god, no
3393 $invoice_data{invoice_lines} = sub { # much better
3394 my $lines = shift || scalar(@buf);
3406 push @collect, split("\n",
3407 $text_template->fill_in( HASH => \%invoice_data )
3409 $invoice_data{'page'}++;
3411 map "$_\n", @collect;
3413 # this is where we actually create the invoice
3414 warn "filling in template for invoice ". $self->invnum. "\n"
3416 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3419 $text_template->fill_in(HASH => \%invoice_data);
3423 # helper routine for generating date ranges
3424 sub _prior_month30s {
3427 [ 1, 2592000 ], # 0-30 days ago
3428 [ 2592000, 5184000 ], # 30-60 days ago
3429 [ 5184000, 7776000 ], # 60-90 days ago
3430 [ 7776000, 0 ], # 90+ days ago
3433 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3434 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3439 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3441 Returns an postscript invoice, as a scalar.
3443 Options can be passed as a hashref (recommended) or as a list of time, template
3444 and then any key/value pairs for any other options.
3446 I<time> an optional value used to control the printing of overdue messages. The
3447 default is now. It isn't the date of the invoice; that's the `_date' field.
3448 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3449 L<Time::Local> and L<Date::Parse> for conversion functions.
3451 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3458 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3459 my $ps = generate_ps($file);
3461 unlink($barcodefile) if $barcodefile;
3466 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3468 Returns an PDF invoice, as a scalar.
3470 Options can be passed as a hashref (recommended) or as a list of time, template
3471 and then any key/value pairs for any other options.
3473 I<time> an optional value used to control the printing of overdue messages. The
3474 default is now. It isn't the date of the invoice; that's the `_date' field.
3475 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3476 L<Time::Local> and L<Date::Parse> for conversion functions.
3478 I<template>, if specified, is the name of a suffix for alternate invoices.
3480 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3487 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3488 my $pdf = generate_pdf($file);
3490 unlink($barcodefile) if $barcodefile;
3495 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3497 Returns an HTML invoice, as a scalar.
3499 I<time> an optional value used to control the printing of overdue messages. The
3500 default is now. It isn't the date of the invoice; that's the `_date' field.
3501 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3502 L<Time::Local> and L<Date::Parse> for conversion functions.
3504 I<template>, if specified, is the name of a suffix for alternate invoices.
3506 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3508 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3509 when emailing the invoice as part of a multipart/related MIME email.
3517 %params = %{ shift() };
3519 $params{'time'} = shift;
3520 $params{'template'} = shift;
3521 $params{'cid'} = shift;
3524 $params{'format'} = 'html';
3526 $self->print_generic( %params );
3529 # quick subroutine for print_latex
3531 # There are ten characters that LaTeX treats as special characters, which
3532 # means that they do not simply typeset themselves:
3533 # # $ % & ~ _ ^ \ { }
3535 # TeX ignores blanks following an escaped character; if you want a blank (as
3536 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3540 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3541 $value =~ s/([<>])/\$$1\$/g;
3547 encode_entities($value);
3551 sub _html_escape_nbsp {
3552 my $value = _html_escape(shift);
3553 $value =~ s/ +/ /g;
3557 #utility methods for print_*
3559 sub _translate_old_latex_format {
3560 warn "_translate_old_latex_format called\n"
3567 if ( $line =~ /^%%Detail\s*$/ ) {
3569 push @template, q![@--!,
3570 q! foreach my $_tr_line (@detail_items) {!,
3571 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3572 q! $_tr_line->{'description'} .= !,
3573 q! "\\tabularnewline\n~~".!,
3574 q! join( "\\tabularnewline\n~~",!,
3575 q! @{$_tr_line->{'ext_description'}}!,
3579 while ( ( my $line_item_line = shift )
3580 !~ /^%%EndDetail\s*$/ ) {
3581 $line_item_line =~ s/'/\\'/g; # nice LTS
3582 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3583 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3584 push @template, " \$OUT .= '$line_item_line';";
3587 push @template, '}',
3590 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3592 push @template, '[@--',
3593 ' foreach my $_tr_line (@total_items) {';
3595 while ( ( my $total_item_line = shift )
3596 !~ /^%%EndTotalDetails\s*$/ ) {
3597 $total_item_line =~ s/'/\\'/g; # nice LTS
3598 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3599 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3600 push @template, " \$OUT .= '$total_item_line';";
3603 push @template, '}',
3607 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3608 push @template, $line;
3614 warn "$_\n" foreach @template;
3622 my $conf = $self->conf;
3624 #check for an invoice-specific override
3625 return $self->invoice_terms if $self->invoice_terms;
3627 #check for a customer- specific override
3628 my $cust_main = $self->cust_main;
3629 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3631 #use configured default
3632 $conf->config('invoice_default_terms') || '';
3638 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3639 $duedate = $self->_date() + ( $1 * 86400 );
3646 $self->due_date ? time2str(shift, $self->due_date) : '';
3649 sub balance_due_msg {
3651 my $msg = $self->mt('Balance Due');
3652 return $msg unless $self->terms;
3653 if ( $self->due_date ) {
3654 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3655 $self->due_date2str($date_format);
3656 } elsif ( $self->terms ) {
3657 $msg .= ' - '. $self->terms;
3662 sub balance_due_date {
3664 my $conf = $self->conf;
3666 if ( $conf->exists('invoice_default_terms')
3667 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3668 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3673 sub credit_balance_msg {
3675 $self->mt('Credit Balance Remaining')
3678 =item invnum_date_pretty
3680 Returns a string with the invoice number and date, for example:
3681 "Invoice #54 (3/20/2008)"
3685 sub invnum_date_pretty {
3687 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3692 Returns a string with the date, for example: "3/20/2008"
3698 time2str($date_format, $self->_date);
3701 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3703 Generate section information for all items appearing on this invoice.
3704 This will only be called for multi-section invoices.
3706 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3707 related display records (L<FS::cust_bill_pkg_display>) and organize
3708 them into two groups ("early" and "late" according to whether they come
3709 before or after the total), then into sections. A subtotal is calculated
3712 Section descriptions are returned in sort weight order. Each consists
3713 of a hash containing:
3715 description: the package category name, escaped
3716 subtotal: the total charges in that section
3717 tax_section: a flag indicating that the section contains only tax charges
3718 summarized: same as tax_section, for some reason
3719 sort_weight: the package category's sort weight
3721 If 'condense' is set on the display record, it also contains everything
3722 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3723 coderefs to generate parts of the invoice. This is not advised.
3727 LATE: an arrayref to push the "late" section hashes onto. The "early"
3728 group is simply returned from the method.
3730 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3731 Turning this on has the following effects:
3732 - Ignores display items with the 'summary' flag.
3733 - Combines all items into the "early" group.
3734 - Creates sections for all non-disabled package categories, even if they
3735 have no charges on this invoice, as well as a section with no name.
3737 ESCAPE: an escape function to use for section titles.
3739 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3740 sorted list. If there are any of these, section subtotals exclude
3743 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3744 passed through to C<_condense_section()>.
3748 use vars qw(%pkg_category_cache);
3749 sub _items_sections {
3752 my $summarypage = shift;
3754 my $extra_sections = shift;
3758 my %late_subtotal = ();
3761 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3764 my $usage = $cust_bill_pkg->usage;
3766 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3767 next if ( $display->summary && $summarypage );
3769 my $section = $display->section;
3770 my $type = $display->type;
3772 $not_tax{$section} = 1
3773 unless $cust_bill_pkg->pkgnum == 0;
3775 if ( $display->post_total && !$summarypage ) {
3776 if (! $type || $type eq 'S') {
3777 $late_subtotal{$section} += $cust_bill_pkg->setup
3778 if $cust_bill_pkg->setup != 0;
3782 $late_subtotal{$section} += $cust_bill_pkg->recur
3783 if $cust_bill_pkg->recur != 0;
3786 if ($type && $type eq 'R') {
3787 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3788 if $cust_bill_pkg->recur != 0;
3791 if ($type && $type eq 'U') {
3792 $late_subtotal{$section} += $usage
3793 unless scalar(@$extra_sections);
3798 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3800 if (! $type || $type eq 'S') {
3801 $subtotal{$section} += $cust_bill_pkg->setup
3802 if $cust_bill_pkg->setup != 0;
3806 $subtotal{$section} += $cust_bill_pkg->recur
3807 if $cust_bill_pkg->recur != 0;
3810 if ($type && $type eq 'R') {
3811 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3812 if $cust_bill_pkg->recur != 0;
3815 if ($type && $type eq 'U') {
3816 $subtotal{$section} += $usage
3817 unless scalar(@$extra_sections);
3826 %pkg_category_cache = ();
3828 push @$late, map { { 'description' => &{$escape}($_),
3829 'subtotal' => $late_subtotal{$_},
3831 'sort_weight' => ( _pkg_category($_)
3832 ? _pkg_category($_)->weight
3835 ((_pkg_category($_) && _pkg_category($_)->condense)
3836 ? $self->_condense_section($format)
3840 sort _sectionsort keys %late_subtotal;
3843 if ( $summarypage ) {
3844 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3845 map { $_->categoryname } qsearch('pkg_category', {});
3846 push @sections, '' if exists($subtotal{''});
3848 @sections = keys %subtotal;
3851 my @early = map { { 'description' => &{$escape}($_),
3852 'subtotal' => $subtotal{$_},
3853 'summarized' => $not_tax{$_} ? '' : 'Y',
3854 'tax_section' => $not_tax{$_} ? '' : 'Y',
3855 'sort_weight' => ( _pkg_category($_)
3856 ? _pkg_category($_)->weight
3859 ((_pkg_category($_) && _pkg_category($_)->condense)
3860 ? $self->_condense_section($format)
3865 push @early, @$extra_sections if $extra_sections;
3867 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3871 #helper subs for above
3874 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3878 my $categoryname = shift;
3879 $pkg_category_cache{$categoryname} ||=
3880 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3883 my %condensed_format = (
3884 'label' => [ qw( Description Qty Amount ) ],
3886 sub { shift->{description} },
3887 sub { shift->{quantity} },
3888 sub { my($href, %opt) = @_;
3889 ($opt{dollar} || ''). $href->{amount};
3892 'align' => [ qw( l r r ) ],
3893 'span' => [ qw( 5 1 1 ) ], # unitprices?
3894 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3897 sub _condense_section {
3898 my ( $self, $format ) = ( shift, shift );
3900 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3901 qw( description_generator
3904 total_line_generator
3909 sub _condensed_generator_defaults {
3910 my ( $self, $format ) = ( shift, shift );
3911 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3920 sub _condensed_header_generator {
3921 my ( $self, $format ) = ( shift, shift );
3923 my ( $f, $prefix, $suffix, $separator, $column ) =
3924 _condensed_generator_defaults($format);
3926 if ($format eq 'latex') {
3927 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3928 $suffix = "\\\\\n\\hline";
3931 sub { my ($d,$a,$s,$w) = @_;
3932 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3934 } elsif ( $format eq 'html' ) {
3935 $prefix = '<th></th>';
3939 sub { my ($d,$a,$s,$w) = @_;
3940 return qq!<th align="$html_align{$a}">$d</th>!;
3948 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3950 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3953 $prefix. join($separator, @result). $suffix;
3958 sub _condensed_description_generator {
3959 my ( $self, $format ) = ( shift, shift );
3961 my ( $f, $prefix, $suffix, $separator, $column ) =
3962 _condensed_generator_defaults($format);
3964 my $money_char = '$';
3965 if ($format eq 'latex') {
3966 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3968 $separator = " & \n";
3970 sub { my ($d,$a,$s,$w) = @_;
3971 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3973 $money_char = '\\dollar';
3974 }elsif ( $format eq 'html' ) {
3975 $prefix = '"><td align="center"></td>';
3979 sub { my ($d,$a,$s,$w) = @_;
3980 return qq!<td align="$html_align{$a}">$d</td>!;
3982 #$money_char = $conf->config('money_char') || '$';
3983 $money_char = ''; # this is madness
3991 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3993 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3995 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3996 map { $f->{$_}->[$i] } qw(align span width)
4000 $prefix. join( $separator, @result ). $suffix;
4005 sub _condensed_total_generator {
4006 my ( $self, $format ) = ( shift, shift );
4008 my ( $f, $prefix, $suffix, $separator, $column ) =
4009 _condensed_generator_defaults($format);
4012 if ($format eq 'latex') {
4015 $separator = " & \n";
4017 sub { my ($d,$a,$s,$w) = @_;
4018 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4020 }elsif ( $format eq 'html' ) {
4024 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4026 sub { my ($d,$a,$s,$w) = @_;
4027 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4036 # my $r = &{$f->{fields}->[$i]}(@args);
4037 # $r .= ' Total' unless $i;
4039 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4041 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4042 map { $f->{$_}->[$i] } qw(align span width)
4046 $prefix. join( $separator, @result ). $suffix;
4051 =item total_line_generator FORMAT
4053 Returns a coderef used for generation of invoice total line items for this
4054 usage_class. FORMAT is either html or latex
4058 # should not be used: will have issues with hash element names (description vs
4059 # total_item and amount vs total_amount -- another array of functions?
4061 sub _condensed_total_line_generator {
4062 my ( $self, $format ) = ( shift, shift );
4064 my ( $f, $prefix, $suffix, $separator, $column ) =
4065 _condensed_generator_defaults($format);
4068 if ($format eq 'latex') {
4071 $separator = " & \n";
4073 sub { my ($d,$a,$s,$w) = @_;
4074 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4076 }elsif ( $format eq 'html' ) {
4080 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4082 sub { my ($d,$a,$s,$w) = @_;
4083 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4092 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4094 &{$column}( &{$f->{fields}->[$i]}(@args),
4095 map { $f->{$_}->[$i] } qw(align span width)
4099 $prefix. join( $separator, @result ). $suffix;
4104 #sub _items_extra_usage_sections {
4106 # my $escape = shift;
4108 # my %sections = ();
4110 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4111 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4113 # next unless $cust_bill_pkg->pkgnum > 0;
4115 # foreach my $section ( keys %usage_class ) {
4117 # my $usage = $cust_bill_pkg->usage($section);
4119 # next unless $usage && $usage > 0;
4121 # $sections{$section} ||= 0;
4122 # $sections{$section} += $usage;
4128 # map { { 'description' => &{$escape}($_),
4129 # 'subtotal' => $sections{$_},
4130 # 'summarized' => '',
4131 # 'tax_section' => '',
4134 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4138 sub _items_extra_usage_sections {
4140 my $conf = $self->conf;
4148 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4150 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4151 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4152 next unless $cust_bill_pkg->pkgnum > 0;
4154 foreach my $classnum ( keys %usage_class ) {
4155 my $section = $usage_class{$classnum}->classname;
4156 $classnums{$section} = $classnum;
4158 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4159 my $amount = $detail->amount;
4160 next unless $amount && $amount > 0;
4162 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4163 $sections{$section}{amount} += $amount; #subtotal
4164 $sections{$section}{calls}++;
4165 $sections{$section}{duration} += $detail->duration;
4167 my $desc = $detail->regionname;
4168 my $description = $desc;
4169 $description = substr($desc, 0, $maxlength). '...'
4170 if $format eq 'latex' && length($desc) > $maxlength;
4172 $lines{$section}{$desc} ||= {
4173 description => &{$escape}($description),
4174 #pkgpart => $part_pkg->pkgpart,
4175 pkgnum => $cust_bill_pkg->pkgnum,
4180 #unit_amount => $cust_bill_pkg->unitrecur,
4181 quantity => $cust_bill_pkg->quantity,
4182 product_code => 'N/A',
4183 ext_description => [],
4186 $lines{$section}{$desc}{amount} += $amount;
4187 $lines{$section}{$desc}{calls}++;
4188 $lines{$section}{$desc}{duration} += $detail->duration;
4194 my %sectionmap = ();
4195 foreach (keys %sections) {
4196 my $usage_class = $usage_class{$classnums{$_}};
4197 $sectionmap{$_} = { 'description' => &{$escape}($_),
4198 'amount' => $sections{$_}{amount}, #subtotal
4199 'calls' => $sections{$_}{calls},
4200 'duration' => $sections{$_}{duration},
4202 'tax_section' => '',
4203 'sort_weight' => $usage_class->weight,
4204 ( $usage_class->format
4205 ? ( map { $_ => $usage_class->$_($format) }
4206 qw( description_generator header_generator total_generator total_line_generator )
4213 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4217 foreach my $section ( keys %lines ) {
4218 foreach my $line ( keys %{$lines{$section}} ) {
4219 my $l = $lines{$section}{$line};
4220 $l->{section} = $sectionmap{$section};
4221 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4222 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4227 return(\@sections, \@lines);
4233 my $end = $self->_date;
4235 # start at date of previous invoice + 1 second or 0 if no previous invoice
4236 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4237 $start = 0 if !$start;
4240 my $cust_main = $self->cust_main;
4241 my @pkgs = $cust_main->all_pkgs;
4242 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4245 foreach my $pkg ( @pkgs ) {
4246 my @h_cust_svc = $pkg->h_cust_svc($end);
4247 foreach my $h_cust_svc ( @h_cust_svc ) {
4248 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4249 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4251 my $inserted = $h_cust_svc->date_inserted;
4252 my $deleted = $h_cust_svc->date_deleted;
4253 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4255 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4257 # DID either activated or ported in; cannot be both for same DID simultaneously
4258 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4259 && (!$phone_inserted->lnp_status
4260 || $phone_inserted->lnp_status eq ''
4261 || $phone_inserted->lnp_status eq 'native')) {
4264 else { # this one not so clean, should probably move to (h_)svc_phone
4265 my $phone_portedin = qsearchs( 'h_svc_phone',
4266 { 'svcnum' => $h_cust_svc->svcnum,
4267 'lnp_status' => 'portedin' },
4268 FS::h_svc_phone->sql_h_searchs($end),
4270 $num_portedin++ if $phone_portedin;
4273 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4274 if($deleted >= $start && $deleted <= $end && $phone_deleted
4275 && (!$phone_deleted->lnp_status
4276 || $phone_deleted->lnp_status ne 'portingout')) {
4279 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4280 && $phone_deleted->lnp_status
4281 && $phone_deleted->lnp_status eq 'portingout') {
4285 # increment usage minutes
4286 if ( $phone_inserted ) {
4287 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4288 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4291 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4294 # don't look at this service again
4295 push @seen, $h_cust_svc->svcnum;
4299 $minutes = sprintf("%d", $minutes);
4300 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4301 . "$num_deactivated Ported-Out: $num_portedout ",
4302 "Total Minutes: $minutes");
4305 sub _items_accountcode_cdr {
4310 my $section = { 'amount' => 0,
4313 'sort_weight' => '',
4315 'description' => 'Usage by Account Code',
4321 my %accountcodes = ();
4323 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4324 next unless $cust_bill_pkg->pkgnum > 0;
4326 my @header = $cust_bill_pkg->details_header;
4327 next unless scalar(@header);
4328 $section->{'header'} = join(',',@header);
4330 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4332 $section->{'header'} = $detail->formatted('format' => $format)
4333 if($detail->detail eq $section->{'header'});
4335 my $accountcode = $detail->accountcode;
4336 next unless $accountcode;
4338 my $amount = $detail->amount;
4339 next unless $amount && $amount > 0;
4341 $accountcodes{$accountcode} ||= {
4342 description => $accountcode,
4349 product_code => 'N/A',
4350 section => $section,
4351 ext_description => [ $section->{'header'} ],
4355 $section->{'amount'} += $amount;
4356 $accountcodes{$accountcode}{'amount'} += $amount;
4357 $accountcodes{$accountcode}{calls}++;
4358 $accountcodes{$accountcode}{duration} += $detail->duration;
4359 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4363 foreach my $l ( values %accountcodes ) {
4364 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4365 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4366 foreach my $sorted_detail ( @sorted_detail ) {
4367 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4369 delete $l->{detail_temp};
4373 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4375 return ($section,\@sorted_lines);
4378 sub _items_svc_phone_sections {
4380 my $conf = $self->conf;
4388 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4390 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4391 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4393 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4394 next unless $cust_bill_pkg->pkgnum > 0;
4396 my @header = $cust_bill_pkg->details_header;
4397 next unless scalar(@header);
4399 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4401 my $phonenum = $detail->phonenum;
4402 next unless $phonenum;
4404 my $amount = $detail->amount;
4405 next unless $amount && $amount > 0;
4407 $sections{$phonenum} ||= { 'amount' => 0,
4410 'sort_weight' => -1,
4411 'phonenum' => $phonenum,
4413 $sections{$phonenum}{amount} += $amount; #subtotal
4414 $sections{$phonenum}{calls}++;
4415 $sections{$phonenum}{duration} += $detail->duration;
4417 my $desc = $detail->regionname;
4418 my $description = $desc;
4419 $description = substr($desc, 0, $maxlength). '...'
4420 if $format eq 'latex' && length($desc) > $maxlength;
4422 $lines{$phonenum}{$desc} ||= {
4423 description => &{$escape}($description),
4424 #pkgpart => $part_pkg->pkgpart,
4432 product_code => 'N/A',
4433 ext_description => [],
4436 $lines{$phonenum}{$desc}{amount} += $amount;
4437 $lines{$phonenum}{$desc}{calls}++;
4438 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4440 my $line = $usage_class{$detail->classnum}->classname;
4441 $sections{"$phonenum $line"} ||=
4445 'sort_weight' => $usage_class{$detail->classnum}->weight,
4446 'phonenum' => $phonenum,
4447 'header' => [ @header ],
4449 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4450 $sections{"$phonenum $line"}{calls}++;
4451 $sections{"$phonenum $line"}{duration} += $detail->duration;
4453 $lines{"$phonenum $line"}{$desc} ||= {
4454 description => &{$escape}($description),
4455 #pkgpart => $part_pkg->pkgpart,
4463 product_code => 'N/A',
4464 ext_description => [],
4467 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4468 $lines{"$phonenum $line"}{$desc}{calls}++;
4469 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4470 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4471 $detail->formatted('format' => $format);
4476 my %sectionmap = ();
4477 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4478 foreach ( keys %sections ) {
4479 my @header = @{ $sections{$_}{header} || [] };
4481 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4482 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4483 my $usage_class = $summary ? $simple : $usage_simple;
4484 my $ending = $summary ? ' usage charges' : '';
4487 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4489 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4490 'amount' => $sections{$_}{amount}, #subtotal
4491 'calls' => $sections{$_}{calls},
4492 'duration' => $sections{$_}{duration},
4494 'tax_section' => '',
4495 'phonenum' => $sections{$_}{phonenum},
4496 'sort_weight' => $sections{$_}{sort_weight},
4497 'post_total' => $summary, #inspire pagebreak
4499 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4500 qw( description_generator
4503 total_line_generator
4510 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4511 $a->{sort_weight} <=> $b->{sort_weight}
4516 foreach my $section ( keys %lines ) {
4517 foreach my $line ( keys %{$lines{$section}} ) {
4518 my $l = $lines{$section}{$line};
4519 $l->{section} = $sectionmap{$section};
4520 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4521 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4526 if($conf->exists('phone_usage_class_summary')) {
4527 # this only works with Latex
4531 # after this, we'll have only two sections per DID:
4532 # Calls Summary and Calls Detail
4533 foreach my $section ( @sections ) {
4534 if($section->{'post_total'}) {
4535 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4536 $section->{'total_line_generator'} = sub { '' };
4537 $section->{'total_generator'} = sub { '' };
4538 $section->{'header_generator'} = sub { '' };
4539 $section->{'description_generator'} = '';
4540 push @newsections, $section;
4541 my %calls_detail = %$section;
4542 $calls_detail{'post_total'} = '';
4543 $calls_detail{'sort_weight'} = '';
4544 $calls_detail{'description_generator'} = sub { '' };
4545 $calls_detail{'header_generator'} = sub {
4546 return ' & Date/Time & Called Number & Duration & Price'
4547 if $format eq 'latex';
4550 $calls_detail{'description'} = 'Calls Detail: '
4551 . $section->{'phonenum'};
4552 push @newsections, \%calls_detail;
4556 # after this, each usage class is collapsed/summarized into a single
4557 # line under the Calls Summary section
4558 foreach my $newsection ( @newsections ) {
4559 if($newsection->{'post_total'}) { # this means Calls Summary
4560 foreach my $section ( @sections ) {
4561 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4562 && !$section->{'post_total'});
4563 my $newdesc = $section->{'description'};
4564 my $tn = $section->{'phonenum'};
4565 $newdesc =~ s/$tn//g;
4566 my $line = { ext_description => [],
4570 calls => $section->{'calls'},
4571 section => $newsection,
4572 duration => $section->{'duration'},
4573 description => $newdesc,
4574 amount => sprintf("%.2f",$section->{'amount'}),
4575 product_code => 'N/A',
4577 push @newlines, $line;
4582 # after this, Calls Details is populated with all CDRs
4583 foreach my $newsection ( @newsections ) {
4584 if(!$newsection->{'post_total'}) { # this means Calls Details
4585 foreach my $line ( @lines ) {
4586 next unless (scalar(@{$line->{'ext_description'}}) &&
4587 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4589 my @extdesc = @{$line->{'ext_description'}};
4591 foreach my $extdesc ( @extdesc ) {
4592 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4593 push @newextdesc, $extdesc;
4595 $line->{'ext_description'} = \@newextdesc;
4596 $line->{'section'} = $newsection;
4597 push @newlines, $line;
4602 return(\@newsections, \@newlines);
4605 return(\@sections, \@lines);
4609 sub _items { # seems to be unused
4612 #my @display = scalar(@_)
4614 # : qw( _items_previous _items_pkg );
4615 # #: qw( _items_pkg );
4616 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4617 my @display = qw( _items_previous _items_pkg );
4620 foreach my $display ( @display ) {
4621 push @b, $self->$display(@_);
4626 sub _items_previous {
4628 my $conf = $self->conf;
4629 my $cust_main = $self->cust_main;
4630 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4632 foreach ( @pr_cust_bill ) {
4633 my $date = $conf->exists('invoice_show_prior_due_date')
4634 ? 'due '. $_->due_date2str($date_format)
4635 : time2str($date_format, $_->_date);
4637 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4638 #'pkgpart' => 'N/A',
4640 'amount' => sprintf("%.2f", $_->owed),
4646 # 'description' => 'Previous Balance',
4647 # #'pkgpart' => 'N/A',
4648 # 'pkgnum' => 'N/A',
4649 # 'amount' => sprintf("%10.2f", $pr_total ),
4650 # 'ext_description' => [ map {
4651 # "Invoice ". $_->invnum.
4652 # " (". time2str("%x",$_->_date). ") ".
4653 # sprintf("%10.2f", $_->owed)
4654 # } @pr_cust_bill ],
4659 =item _items_pkg [ OPTIONS ]
4661 Return line item hashes for each package item on this invoice. Nearly
4664 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4666 The only OPTIONS accepted is 'section', which may point to a hashref
4667 with a key named 'condensed', which may have a true value. If it
4668 does, this method tries to merge identical items into items with
4669 'quantity' equal to the number of items (not the sum of their
4670 separate quantities, for some reason).
4678 warn "$me _items_pkg searching for all package line items\n"
4681 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4683 warn "$me _items_pkg filtering line items\n"
4685 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4687 if ($options{section} && $options{section}->{condensed}) {
4689 warn "$me _items_pkg condensing section\n"
4693 local $Storable::canonical = 1;
4694 foreach ( @items ) {
4696 delete $item->{ref};
4697 delete $item->{ext_description};
4698 my $key = freeze($item);
4699 $itemshash{$key} ||= 0;
4700 $itemshash{$key} ++; # += $item->{quantity};
4702 @items = sort { $a->{description} cmp $b->{description} }
4703 map { my $i = thaw($_);
4704 $i->{quantity} = $itemshash{$_};
4706 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4712 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4719 return 0 unless $a->itemdesc cmp $b->itemdesc;
4720 return -1 if $b->itemdesc eq 'Tax';
4721 return 1 if $a->itemdesc eq 'Tax';
4722 return -1 if $b->itemdesc eq 'Other surcharges';
4723 return 1 if $a->itemdesc eq 'Other surcharges';
4724 $a->itemdesc cmp $b->itemdesc;
4729 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4730 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4733 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4735 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4736 list of hashrefs describing the line items they generate on the invoice.
4738 OPTIONS may include:
4740 format: the invoice format.
4742 escape_function: the function used to escape strings.
4744 format_function: the function used to format CDRs.
4746 section: a hashref containing 'description'; if this is present,
4747 cust_bill_pkg_display records not belonging to this section are
4750 multisection: a flag indicating that this is a multisection invoice,
4751 which does something complicated.
4753 multilocation: a flag to display the location label for the package.
4755 Returns a list of hashrefs, each of which may contain:
4757 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4758 ext_description, which is an arrayref of detail lines to show below
4763 sub _items_cust_bill_pkg {
4765 my $conf = $self->conf;
4766 my $cust_bill_pkgs = shift;
4769 my $format = $opt{format} || '';
4770 my $escape_function = $opt{escape_function} || sub { shift };
4771 my $format_function = $opt{format_function} || '';
4772 my $unsquelched = $opt{unsquelched} || ''; #unused
4773 my $section = $opt{section}->{description} if $opt{section};
4774 my $summary_page = $opt{summary_page} || ''; #unused
4775 my $multilocation = $opt{multilocation} || '';
4776 my $multisection = $opt{multisection} || '';
4777 my $discount_show_always = 0;
4779 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4782 my ($s, $r, $u) = ( undef, undef, undef );
4783 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4786 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4787 if ( $_ && !$cust_bill_pkg->hidden ) {
4788 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4789 $_->{amount} =~ s/^\-0\.00$/0.00/;
4790 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4792 if $_->{amount} != 0
4793 || $discount_show_always
4794 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4795 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4801 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4802 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4805 foreach my $display ( grep { defined($section)
4806 ? $_->section eq $section
4809 #grep { !$_->summary || !$summary_page } # bunk!
4810 grep { !$_->summary || $multisection }
4811 $cust_bill_pkg->cust_bill_pkg_display
4815 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4816 $display->billpkgdisplaynum. "\n"
4819 my $type = $display->type;
4821 my $desc = $cust_bill_pkg->desc;
4822 $desc = substr($desc, 0, $maxlength). '...'
4823 if $format eq 'latex' && length($desc) > $maxlength;
4825 my %details_opt = ( 'format' => $format,
4826 'escape_function' => $escape_function,
4827 'format_function' => $format_function,
4830 if ( $cust_bill_pkg->pkgnum > 0 ) {
4832 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4835 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4837 # start/end dates for invoice formats that do nonstandard
4839 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4841 if ( (!$type || $type eq 'S')
4842 && ( $cust_bill_pkg->setup != 0
4843 || $cust_bill_pkg->setup_show_zero
4848 warn "$me _items_cust_bill_pkg adding setup\n"
4851 my $description = $desc;
4852 $description .= ' Setup'
4853 if $cust_bill_pkg->recur != 0
4854 || $discount_show_always
4855 || $cust_bill_pkg->recur_show_zero;
4858 unless ( $cust_pkg->part_pkg->hide_svc_detail
4859 || $cust_bill_pkg->hidden )
4862 push @d, map &{$escape_function}($_),
4863 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4864 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4866 if ( $multilocation ) {
4867 my $loc = $cust_pkg->location_label;
4868 $loc = substr($loc, 0, $maxlength). '...'
4869 if $format eq 'latex' && length($loc) > $maxlength;
4870 push @d, &{$escape_function}($loc);
4873 } #unless hiding service details
4875 push @d, $cust_bill_pkg->details(%details_opt)
4876 if $cust_bill_pkg->recur == 0;
4878 if ( $cust_bill_pkg->hidden ) {
4879 $s->{amount} += $cust_bill_pkg->setup;
4880 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4881 push @{ $s->{ext_description} }, @d;
4885 description => $description,
4886 #pkgpart => $part_pkg->pkgpart,
4887 pkgnum => $cust_bill_pkg->pkgnum,
4888 amount => $cust_bill_pkg->setup,
4889 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4890 unit_amount => $cust_bill_pkg->unitsetup,
4891 quantity => $cust_bill_pkg->quantity,
4892 ext_description => \@d,
4898 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4900 $cust_bill_pkg->recur != 0
4901 || $cust_bill_pkg->setup == 0
4902 || $discount_show_always
4903 || $cust_bill_pkg->recur_show_zero
4908 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4911 my $is_summary = $display->summary;
4912 my $description = ($is_summary && $type && $type eq 'U')
4913 ? "Usage charges" : $desc;
4915 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4916 " - ". time2str($date_format, $cust_bill_pkg->edate).
4918 unless $conf->exists('disable_line_item_date_ranges')
4919 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4922 my @seconds = (); # for display of usage info
4924 #at least until cust_bill_pkg has "past" ranges in addition to
4925 #the "future" sdate/edate ones... see #3032
4926 my @dates = ( $self->_date );
4927 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4928 push @dates, $prev->sdate if $prev;
4929 push @dates, undef if !$prev;
4931 unless ( $cust_pkg->part_pkg->hide_svc_detail
4932 || $cust_bill_pkg->itemdesc
4933 || $cust_bill_pkg->hidden
4934 || $is_summary && $type && $type eq 'U' )
4937 warn "$me _items_cust_bill_pkg adding service details\n"
4940 push @d, map &{$escape_function}($_),
4941 $cust_pkg->h_labels_short(@dates, 'I')
4942 #$cust_bill_pkg->edate,
4943 #$cust_bill_pkg->sdate)
4944 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4946 warn "$me _items_cust_bill_pkg done adding service details\n"
4949 if ( $multilocation ) {
4950 my $loc = $cust_pkg->location_label;
4951 $loc = substr($loc, 0, $maxlength). '...'
4952 if $format eq 'latex' && length($loc) > $maxlength;
4953 push @d, &{$escape_function}($loc);
4956 # Display of seconds_since_sqlradacct:
4957 # On the invoice, when processing @detail_items, look for a field
4958 # named 'seconds'. This will contain total seconds for each
4959 # service, in the same order as @ext_description. For services
4960 # that don't support this it will show undef.
4961 if ( $conf->exists('svc_acct-usage_seconds')
4962 and ! $cust_bill_pkg->pkgpart_override ) {
4963 foreach my $cust_svc (
4964 $cust_pkg->h_cust_svc(@dates, 'I')
4967 # eval because not having any part_export_usage exports
4968 # is a fatal error, last_bill/_date because that's how
4969 # sqlradius_hour billing does it
4971 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4973 push @seconds, $sec;
4975 } #if svc_acct-usage_seconds
4979 unless ( $is_summary ) {
4980 warn "$me _items_cust_bill_pkg adding details\n"
4983 #instead of omitting details entirely in this case (unwanted side
4984 # effects), just omit CDRs
4985 $details_opt{'format_function'} = sub { () }
4986 if $type && $type eq 'R';
4988 push @d, $cust_bill_pkg->details(%details_opt);
4991 warn "$me _items_cust_bill_pkg calculating amount\n"
4996 $amount = $cust_bill_pkg->recur;
4997 } elsif ($type eq 'R') {
4998 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4999 } elsif ($type eq 'U') {
5000 $amount = $cust_bill_pkg->usage;
5003 if ( !$type || $type eq 'R' ) {
5005 warn "$me _items_cust_bill_pkg adding recur\n"
5008 if ( $cust_bill_pkg->hidden ) {
5009 $r->{amount} += $amount;
5010 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5011 push @{ $r->{ext_description} }, @d;
5014 description => $description,
5015 #pkgpart => $part_pkg->pkgpart,
5016 pkgnum => $cust_bill_pkg->pkgnum,
5018 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5019 unit_amount => $cust_bill_pkg->unitrecur,
5020 quantity => $cust_bill_pkg->quantity,
5022 ext_description => \@d,
5024 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5027 } else { # $type eq 'U'
5029 warn "$me _items_cust_bill_pkg adding usage\n"
5032 if ( $cust_bill_pkg->hidden ) {
5033 $u->{amount} += $amount;
5034 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5035 push @{ $u->{ext_description} }, @d;
5038 description => $description,
5039 #pkgpart => $part_pkg->pkgpart,
5040 pkgnum => $cust_bill_pkg->pkgnum,
5042 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5043 unit_amount => $cust_bill_pkg->unitrecur,
5044 quantity => $cust_bill_pkg->quantity,
5046 ext_description => \@d,
5051 } # recurring or usage with recurring charge
5053 } else { #pkgnum tax or one-shot line item (??)
5055 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5058 if ( $cust_bill_pkg->setup != 0 ) {
5060 'description' => $desc,
5061 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5064 if ( $cust_bill_pkg->recur != 0 ) {
5066 'description' => "$desc (".
5067 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5068 time2str($date_format, $cust_bill_pkg->edate). ')',
5069 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5077 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5078 && $conf->exists('discount-show-always'));
5082 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5084 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5085 $_->{amount} =~ s/^\-0\.00$/0.00/;
5086 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5088 if $_->{amount} != 0
5089 || $discount_show_always
5090 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5091 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5095 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5102 sub _items_credits {
5103 my( $self, %opt ) = @_;
5104 my $trim_len = $opt{'trim_len'} || 60;
5108 foreach ( $self->cust_credited ) {
5110 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5112 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5113 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5114 $reason = " ($reason) " if $reason;
5117 #'description' => 'Credit ref\#'. $_->crednum.
5118 # " (". time2str("%x",$_->cust_credit->_date) .")".
5120 'description' => $self->mt('Credit applied').' '.
5121 time2str($date_format,$_->cust_credit->_date). $reason,
5122 'amount' => sprintf("%.2f",$_->amount),
5130 sub _items_payments {
5134 #get & print payments
5135 foreach ( $self->cust_bill_pay ) {
5137 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5140 'description' => $self->mt('Payment received').' '.
5141 time2str($date_format,$_->cust_pay->_date ),
5142 'amount' => sprintf("%.2f", $_->amount )
5150 =item call_details [ OPTION => VALUE ... ]
5152 Returns an array of CSV strings representing the call details for this invoice
5153 The only option available is the boolean prepend_billed_number
5158 my ($self, %opt) = @_;
5160 my $format_function = sub { shift };
5162 if ($opt{prepend_billed_number}) {
5163 $format_function = sub {
5167 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5172 my @details = map { $_->details( 'format_function' => $format_function,
5173 'escape_function' => sub{ return() },
5177 $self->cust_bill_pkg;
5178 my $header = $details[0];
5179 ( $header, grep { $_ ne $header } @details );
5189 =item process_reprint
5193 sub process_reprint {
5194 process_re_X('print', @_);
5197 =item process_reemail
5201 sub process_reemail {
5202 process_re_X('email', @_);
5210 process_re_X('fax', @_);
5218 process_re_X('ftp', @_);
5225 sub process_respool {
5226 process_re_X('spool', @_);
5229 use Storable qw(thaw);
5233 my( $method, $job ) = ( shift, shift );
5234 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5236 my $param = thaw(decode_base64(shift));
5237 warn Dumper($param) if $DEBUG;
5248 my($method, $job, %param ) = @_;
5250 warn "re_X $method for job $job with param:\n".
5251 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5254 #some false laziness w/search/cust_bill.html
5256 my $orderby = 'ORDER BY cust_bill._date';
5258 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5260 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5262 my @cust_bill = qsearch( {
5263 #'select' => "cust_bill.*",
5264 'table' => 'cust_bill',
5265 'addl_from' => $addl_from,
5267 'extra_sql' => $extra_sql,
5268 'order_by' => $orderby,
5272 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5274 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5277 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5278 foreach my $cust_bill ( @cust_bill ) {
5279 $cust_bill->$method();
5281 if ( $job ) { #progressbar foo
5283 if ( time - $min_sec > $last ) {
5284 my $error = $job->update_statustext(
5285 int( 100 * $num / scalar(@cust_bill) )
5287 die $error if $error;
5298 =head1 CLASS METHODS
5304 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5309 my ($class, $start, $end) = @_;
5311 $class->paid_sql($start, $end). ' - '.
5312 $class->credited_sql($start, $end);
5317 Returns an SQL fragment to retreive the net amount (charged minus credited).
5322 my ($class, $start, $end) = @_;
5323 'charged - '. $class->credited_sql($start, $end);
5328 Returns an SQL fragment to retreive the amount paid against this invoice.
5333 my ($class, $start, $end) = @_;
5334 $start &&= "AND cust_bill_pay._date <= $start";
5335 $end &&= "AND cust_bill_pay._date > $end";
5336 $start = '' unless defined($start);
5337 $end = '' unless defined($end);
5338 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5339 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5344 Returns an SQL fragment to retreive the amount credited against this invoice.
5349 my ($class, $start, $end) = @_;
5350 $start &&= "AND cust_credit_bill._date <= $start";
5351 $end &&= "AND cust_credit_bill._date > $end";
5352 $start = '' unless defined($start);
5353 $end = '' unless defined($end);
5354 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5355 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5360 Returns an SQL fragment to retrieve the due date of an invoice.
5361 Currently only supported on PostgreSQL.
5366 my $conf = new FS::Conf;
5370 cust_bill.invoice_terms,
5371 cust_main.invoice_terms,
5372 \''.($conf->config('invoice_default_terms') || '').'\'
5373 ), E\'Net (\\\\d+)\'
5375 ) * 86400 + cust_bill._date'
5378 =item search_sql_where HASHREF
5380 Class method which returns an SQL WHERE fragment to search for parameters
5381 specified in HASHREF. Valid parameters are
5387 List reference of start date, end date, as UNIX timestamps.
5397 List reference of charged limits (exclusive).
5401 List reference of charged limits (exclusive).
5405 flag, return open invoices only
5409 flag, return net invoices only
5413 =item newest_percust
5417 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5421 sub search_sql_where {
5422 my($class, $param) = @_;
5424 warn "$me search_sql_where called with params: \n".
5425 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5431 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5432 push @search, "cust_main.agentnum = $1";
5436 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5437 push @search, "cust_bill.custnum = $1";
5441 if ( $param->{_date} ) {
5442 my($beginning, $ending) = @{$param->{_date}};
5444 push @search, "cust_bill._date >= $beginning",
5445 "cust_bill._date < $ending";
5449 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5450 push @search, "cust_bill.invnum >= $1";
5452 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5453 push @search, "cust_bill.invnum <= $1";
5457 if ( $param->{charged} ) {
5458 my @charged = ref($param->{charged})
5459 ? @{ $param->{charged} }
5460 : ($param->{charged});
5462 push @search, map { s/^charged/cust_bill.charged/; $_; }
5466 my $owed_sql = FS::cust_bill->owed_sql;
5469 if ( $param->{owed} ) {
5470 my @owed = ref($param->{owed})
5471 ? @{ $param->{owed} }
5473 push @search, map { s/^owed/$owed_sql/; $_; }
5478 push @search, "0 != $owed_sql"
5479 if $param->{'open'};
5480 push @search, '0 != '. FS::cust_bill->net_sql
5484 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5485 if $param->{'days'};
5488 if ( $param->{'newest_percust'} ) {
5490 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5491 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5493 my @newest_where = map { my $x = $_;
5494 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5497 grep ! /^cust_main./, @search;
5498 my $newest_where = scalar(@newest_where)
5499 ? ' AND '. join(' AND ', @newest_where)
5503 push @search, "cust_bill._date = (
5504 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5505 WHERE newest_cust_bill.custnum = cust_bill.custnum
5511 #agent virtualization
5512 my $curuser = $FS::CurrentUser::CurrentUser;
5513 if ( $curuser->username eq 'fs_queue'
5514 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5516 my $newuser = qsearchs('access_user', {
5517 'username' => $username,
5521 $curuser = $newuser;
5523 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5526 push @search, $curuser->agentnums_sql;
5528 join(' AND ', @search );
5540 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5541 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base