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 # prototype here to silence warnings
2666 $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
2667 # eventually use this date handle everywhere in here, too
2669 my $min_sdate = 999999999999;
2671 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2672 next unless $cust_bill_pkg->pkgnum > 0;
2673 $min_sdate = $cust_bill_pkg->sdate
2674 if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2675 $max_edate = $cust_bill_pkg->edate
2676 if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2679 $invoice_data{'bill_period'} = '';
2680 $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate)
2681 . " to " . time2str('%e %h', $max_edate)
2682 if ($max_edate != 0 && $min_sdate != 999999999999);
2684 $invoice_data{finance_section} = '';
2685 if ( $conf->config('finance_pkgclass') ) {
2687 qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2688 $invoice_data{finance_section} = $pkg_class->categoryname;
2690 $invoice_data{finance_amount} = '0.00';
2691 $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2693 my $countrydefault = $conf->config('countrydefault') || 'US';
2694 my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2695 foreach ( qw( contact company address1 address2 city state zip country fax) ){
2696 my $method = $prefix.$_;
2697 $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2699 $invoice_data{'ship_country'} = ''
2700 if ( $invoice_data{'ship_country'} eq $countrydefault );
2702 $invoice_data{'cid'} = $params{'cid'}
2705 if ( $cust_main->country eq $countrydefault ) {
2706 $invoice_data{'country'} = '';
2708 $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2712 $invoice_data{'address'} = \@address;
2714 $cust_main->payname.
2715 ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2716 ? " (P.O. #". $cust_main->payinfo. ")"
2720 push @address, $cust_main->company
2721 if $cust_main->company;
2722 push @address, $cust_main->address1;
2723 push @address, $cust_main->address2
2724 if $cust_main->address2;
2726 $cust_main->city. ", ". $cust_main->state. " ". $cust_main->zip;
2727 push @address, $invoice_data{'country'}
2728 if $invoice_data{'country'};
2730 while (scalar(@address) < 5);
2732 $invoice_data{'logo_file'} = $params{'logo_file'}
2733 if $params{'logo_file'};
2734 $invoice_data{'barcode_file'} = $params{'barcode_file'}
2735 if $params{'barcode_file'};
2736 $invoice_data{'barcode_img'} = $params{'barcode_img'}
2737 if $params{'barcode_img'};
2738 $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2739 if $params{'barcode_cid'};
2741 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2742 # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2743 #my $balance_due = $self->owed + $pr_total - $cr_total;
2744 my $balance_due = $self->owed + $pr_total;
2746 # the customer's current balance as shown on the invoice before this one
2747 $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2749 # the change in balance from that invoice to this one
2750 $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2752 # the sum of amount owed on all previous invoices
2753 $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2755 # the sum of amount owed on all invoices
2756 $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2758 # info from customer's last invoice before this one, for some
2760 $invoice_data{'last_bill'} = {};
2761 my $last_bill = $pr_cust_bill[-1];
2763 $invoice_data{'last_bill'} = {
2764 '_date' => $last_bill->_date, #unformatted
2765 # all we need for now
2769 my $summarypage = '';
2770 if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2773 $invoice_data{'summarypage'} = $summarypage;
2775 warn "$me substituting variables in notes, footer, smallfooter\n"
2778 my @include = (qw( notes footer smallfooter ));
2779 push @include, 'coupon' unless $params{'no_coupon'};
2780 foreach my $include (@include) {
2782 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2785 if ( $conf->exists($inc_file, $agentnum)
2786 && length( $conf->config($inc_file, $agentnum) ) ) {
2788 @inc_src = $conf->config($inc_file, $agentnum);
2792 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2794 my $convert_map = $convert_maps{$format}{$include};
2796 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2797 s/--\@\]/$delimiters{$format}[1]/g;
2800 &$convert_map( $conf->config($inc_file, $agentnum) );
2804 my $inc_tt = new Text::Template (
2806 SOURCE => [ map "$_\n", @inc_src ],
2807 DELIMITERS => $delimiters{$format},
2808 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2810 unless ( $inc_tt->compile() ) {
2811 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2812 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2816 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2818 $invoice_data{$include} =~ s/\n+$//
2819 if ($format eq 'latex');
2822 # let invoices use either of these as needed
2823 $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL')
2824 ? $cust_main->payinfo : '';
2825 $invoice_data{'po_line'} =
2826 ( $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2827 ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
2830 my %money_chars = ( 'latex' => '',
2831 'html' => $conf->config('money_char') || '$',
2834 my $money_char = $money_chars{$format};
2836 my %other_money_chars = ( 'latex' => '\dollar ',#XXX should be a config too
2837 'html' => $conf->config('money_char') || '$',
2840 my $other_money_char = $other_money_chars{$format};
2841 $invoice_data{'dollar'} = $other_money_char;
2843 my @detail_items = ();
2844 my @total_items = ();
2848 $invoice_data{'detail_items'} = \@detail_items;
2849 $invoice_data{'total_items'} = \@total_items;
2850 $invoice_data{'buf'} = \@buf;
2851 $invoice_data{'sections'} = \@sections;
2853 warn "$me generating sections\n"
2856 my $previous_section = { 'description' => $self->mt('Previous Charges'),
2857 'subtotal' => $other_money_char.
2858 sprintf('%.2f', $pr_total),
2859 'summarized' => $summarypage ? 'Y' : '',
2861 $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '.
2862 join(' / ', map { $cust_main->balance_date_range(@$_) }
2863 $self->_prior_month30s
2865 if $conf->exists('invoice_include_aging');
2868 my $tax_section = { 'description' => $self->mt('Taxes, Surcharges, and Fees'),
2869 'subtotal' => $taxtotal, # adjusted below
2870 'summarized' => $summarypage ? 'Y' : '',
2872 my $tax_weight = _pkg_category($tax_section->{description})
2873 ? _pkg_category($tax_section->{description})->weight
2875 $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2876 $tax_section->{'sort_weight'} = $tax_weight;
2879 my $adjusttotal = 0;
2880 my $adjust_section = { 'description' =>
2881 $self->mt('Credits, Payments, and Adjustments'),
2882 'subtotal' => 0, # adjusted below
2883 'summarized' => $summarypage ? 'Y' : '',
2885 my $adjust_weight = _pkg_category($adjust_section->{description})
2886 ? _pkg_category($adjust_section->{description})->weight
2888 $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2889 $adjust_section->{'sort_weight'} = $adjust_weight;
2891 my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2892 my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2893 $invoice_data{'multisection'} = $multisection;
2894 my $late_sections = [];
2895 my $extra_sections = [];
2896 my $extra_lines = ();
2897 if ( $multisection ) {
2898 ($extra_sections, $extra_lines) =
2899 $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2900 if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2902 push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2904 push @detail_items, @$extra_lines if $extra_lines;
2906 $self->_items_sections( $late_sections, # this could stand a refactor
2908 $escape_function_nonbsp,
2912 if ($conf->exists('svc_phone_sections')) {
2913 my ($phone_sections, $phone_lines) =
2914 $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2915 push @{$late_sections}, @$phone_sections;
2916 push @detail_items, @$phone_lines;
2918 if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
2919 my ($accountcode_section, $accountcode_lines) =
2920 $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
2921 if ( scalar(@$accountcode_lines) ) {
2922 push @{$late_sections}, $accountcode_section;
2923 push @detail_items, @$accountcode_lines;
2926 } else {# not multisection
2927 # make a default section
2928 push @sections, { 'description' => '', 'subtotal' => '',
2929 'no_subtotal' => 1 };
2930 # and calculate the finance charge total, since it won't get done otherwise.
2931 # XXX possibly other totals?
2932 # XXX possibly finance_pkgclass should not be used in this manner?
2933 if ( $conf->exists('finance_pkgclass') ) {
2934 my @finance_charges;
2935 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2936 if ( grep { $_->section eq $invoice_data{finance_section} }
2937 $cust_bill_pkg->cust_bill_pkg_display ) {
2938 # I think these are always setup fees, but just to be sure...
2939 push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
2942 $invoice_data{finance_amount} =
2943 sprintf('%.2f', sum( @finance_charges ) || 0);
2947 unless ( $conf->exists('disable_previous_balance')
2948 || $conf->exists('previous_balance-summary_only')
2952 warn "$me adding previous balances\n"
2955 foreach my $line_item ( $self->_items_previous ) {
2958 ext_description => [],
2960 $detail->{'ref'} = $line_item->{'pkgnum'};
2961 $detail->{'quantity'} = 1;
2962 $detail->{'section'} = $previous_section;
2963 $detail->{'description'} = &$escape_function($line_item->{'description'});
2964 if ( exists $line_item->{'ext_description'} ) {
2965 @{$detail->{'ext_description'}} = map {
2966 &$escape_function($_);
2967 } @{$line_item->{'ext_description'}};
2969 $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2970 $line_item->{'amount'};
2971 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2973 push @detail_items, $detail;
2974 push @buf, [ $detail->{'description'},
2975 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2981 if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2982 push @buf, ['','-----------'];
2983 push @buf, [ $self->mt('Total Previous Balance'),
2984 $money_char. sprintf("%10.2f", $pr_total) ];
2988 if ( $conf->exists('svc_phone-did-summary') ) {
2989 warn "$me adding DID summary\n"
2992 my ($didsummary,$minutes) = $self->_did_summary;
2993 my $didsummary_desc = 'DID Activity Summary (since last invoice)';
2995 { 'description' => $didsummary_desc,
2996 'ext_description' => [ $didsummary, $minutes ],
3000 foreach my $section (@sections, @$late_sections) {
3002 warn "$me adding section \n". Dumper($section)
3005 # begin some normalization
3006 $section->{'subtotal'} = $section->{'amount'}
3008 && !exists($section->{subtotal})
3009 && exists($section->{amount});
3011 $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
3012 if ( $invoice_data{finance_section} &&
3013 $section->{'description'} eq $invoice_data{finance_section} );
3015 $section->{'subtotal'} = $other_money_char.
3016 sprintf('%.2f', $section->{'subtotal'})
3019 # continue some normalization
3020 $section->{'amount'} = $section->{'subtotal'}
3024 if ( $section->{'description'} ) {
3025 push @buf, ( [ &$escape_function($section->{'description'}), '' ],
3030 warn "$me setting options\n"
3033 my $multilocation = scalar($cust_main->cust_location); #too expensive?
3035 $options{'section'} = $section if $multisection;
3036 $options{'format'} = $format;
3037 $options{'escape_function'} = $escape_function;
3038 $options{'format_function'} = sub { () } unless $unsquelched;
3039 $options{'unsquelched'} = $unsquelched;
3040 $options{'summary_page'} = $summarypage;
3041 $options{'skip_usage'} =
3042 scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
3043 $options{'multilocation'} = $multilocation;
3044 $options{'multisection'} = $multisection;
3046 warn "$me searching for line items\n"
3049 foreach my $line_item ( $self->_items_pkg(%options) ) {
3051 warn "$me adding line item $line_item\n"
3055 ext_description => [],
3057 $detail->{'ref'} = $line_item->{'pkgnum'};
3058 $detail->{'quantity'} = $line_item->{'quantity'};
3059 $detail->{'section'} = $section;
3060 $detail->{'description'} = &$escape_function($line_item->{'description'});
3061 if ( exists $line_item->{'ext_description'} ) {
3062 @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
3064 $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
3065 $line_item->{'amount'};
3066 $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
3067 $line_item->{'unit_amount'};
3068 $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
3070 $detail->{'sdate'} = $line_item->{'sdate'};
3071 $detail->{'edate'} = $line_item->{'edate'};
3072 $detail->{'seconds'} = $line_item->{'seconds'};
3074 push @detail_items, $detail;
3075 push @buf, ( [ $detail->{'description'},
3076 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
3078 map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
3082 if ( $section->{'description'} ) {
3083 push @buf, ( ['','-----------'],
3084 [ $section->{'description'}. ' sub-total',
3085 $section->{'subtotal'} # already formatted this
3094 $invoice_data{current_less_finance} =
3095 sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
3097 if ( $multisection && !$conf->exists('disable_previous_balance')
3098 || $conf->exists('previous_balance-summary_only') )
3100 unshift @sections, $previous_section if $pr_total;
3103 warn "$me adding taxes\n"
3106 foreach my $tax ( $self->_items_tax ) {
3108 $taxtotal += $tax->{'amount'};
3110 my $description = &$escape_function( $tax->{'description'} );
3111 my $amount = sprintf( '%.2f', $tax->{'amount'} );
3113 if ( $multisection ) {
3115 my $money = $old_latex ? '' : $money_char;
3116 push @detail_items, {
3117 ext_description => [],
3120 description => $description,
3121 amount => $money. $amount,
3123 section => $tax_section,
3128 push @total_items, {
3129 'total_item' => $description,
3130 'total_amount' => $other_money_char. $amount,
3135 push @buf,[ $description,
3136 $money_char. $amount,
3143 $total->{'total_item'} = $self->mt('Sub-total');
3144 $total->{'total_amount'} =
3145 $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
3147 if ( $multisection ) {
3148 $tax_section->{'subtotal'} = $other_money_char.
3149 sprintf('%.2f', $taxtotal);
3150 $tax_section->{'pretotal'} = 'New charges sub-total '.
3151 $total->{'total_amount'};
3152 push @sections, $tax_section if $taxtotal;
3154 unshift @total_items, $total;
3157 $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3159 push @buf,['','-----------'];
3160 push @buf,[$self->mt(
3161 $conf->exists('disable_previous_balance')
3163 : 'Total New Charges'
3165 $money_char. sprintf("%10.2f",$self->charged) ];
3171 $item = $conf->config('previous_balance-exclude_from_total')
3172 || 'Total New Charges'
3173 if $conf->exists('previous_balance-exclude_from_total');
3174 my $amount = $self->charged +
3175 ( $conf->exists('disable_previous_balance') ||
3176 $conf->exists('previous_balance-exclude_from_total')
3180 $total->{'total_item'} = &$embolden_function($self->mt($item));
3181 $total->{'total_amount'} =
3182 &$embolden_function( $other_money_char. sprintf( '%.2f', $amount ) );
3183 if ( $multisection ) {
3184 if ( $adjust_section->{'sort_weight'} ) {
3185 $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
3186 $other_money_char. sprintf("%.2f", ($self->billing_balance || 0) );
3188 $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
3189 $other_money_char. sprintf('%.2f', $self->charged );
3192 push @total_items, $total;
3194 push @buf,['','-----------'];
3197 sprintf( '%10.2f', $amount )
3202 unless ( $conf->exists('disable_previous_balance') ) {
3203 #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3206 my $credittotal = 0;
3207 foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3210 $total->{'total_item'} = &$escape_function($credit->{'description'});
3211 $credittotal += $credit->{'amount'};
3212 $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3213 $adjusttotal += $credit->{'amount'};
3214 if ( $multisection ) {
3215 my $money = $old_latex ? '' : $money_char;
3216 push @detail_items, {
3217 ext_description => [],
3220 description => &$escape_function($credit->{'description'}),
3221 amount => $money. $credit->{'amount'},
3223 section => $adjust_section,
3226 push @total_items, $total;
3230 $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3233 foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3234 push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3238 my $paymenttotal = 0;
3239 foreach my $payment ( $self->_items_payments ) {
3241 $total->{'total_item'} = &$escape_function($payment->{'description'});
3242 $paymenttotal += $payment->{'amount'};
3243 $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3244 $adjusttotal += $payment->{'amount'};
3245 if ( $multisection ) {
3246 my $money = $old_latex ? '' : $money_char;
3247 push @detail_items, {
3248 ext_description => [],
3251 description => &$escape_function($payment->{'description'}),
3252 amount => $money. $payment->{'amount'},
3254 section => $adjust_section,
3257 push @total_items, $total;
3259 push @buf, [ $payment->{'description'},
3260 $money_char. sprintf("%10.2f", $payment->{'amount'}),
3263 $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3265 if ( $multisection ) {
3266 $adjust_section->{'subtotal'} = $other_money_char.
3267 sprintf('%.2f', $adjusttotal);
3268 push @sections, $adjust_section
3269 unless $adjust_section->{sort_weight};
3272 # create Balance Due message
3275 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3276 $total->{'total_amount'} =
3277 &$embolden_function(
3278 $other_money_char. sprintf('%.2f', $summarypage
3280 $self->billing_balance
3281 : $self->owed + $pr_total
3284 if ( $multisection && !$adjust_section->{sort_weight} ) {
3285 $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3286 $total->{'total_amount'};
3288 push @total_items, $total;
3290 push @buf,['','-----------'];
3291 push @buf,[$self->balance_due_msg, $money_char.
3292 sprintf("%10.2f", $balance_due ) ];
3295 if ( $conf->exists('previous_balance-show_credit')
3296 and $cust_main->balance < 0 ) {
3297 my $credit_total = {
3298 'total_item' => &$embolden_function($self->credit_balance_msg),
3299 'total_amount' => &$embolden_function(
3300 $other_money_char. sprintf('%.2f', -$cust_main->balance)
3303 if ( $multisection ) {
3304 $adjust_section->{'posttotal'} .= $newline_token .
3305 $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3308 push @total_items, $credit_total;
3310 push @buf,['','-----------'];
3311 push @buf,[$self->credit_balance_msg, $money_char.
3312 sprintf("%10.2f", -$cust_main->balance ) ];
3316 if ( $multisection ) {
3317 if ($conf->exists('svc_phone_sections')) {
3319 $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3320 $total->{'total_amount'} =
3321 &$embolden_function(
3322 $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3324 my $last_section = pop @sections;
3325 $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3326 $total->{'total_amount'};
3327 push @sections, $last_section;
3329 push @sections, @$late_sections
3333 # make a discounts-available section, even without multisection
3334 if ( $conf->exists('discount-show_available')
3335 and my @discounts_avail = $self->_items_discounts_avail ) {
3336 my $discount_section = {
3337 'description' => $self->mt('Discounts Available'),
3342 push @sections, $discount_section;
3343 push @detail_items, map { +{
3344 'ref' => '', #should this be something else?
3345 'section' => $discount_section,
3346 'description' => &$escape_function( $_->{description} ),
3347 'amount' => $money_char . &$escape_function( $_->{amount} ),
3348 'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
3349 } } @discounts_avail;
3352 # All sections and items are built; now fill in templates.
3353 my @includelist = ();
3354 push @includelist, 'summary' if $summarypage;
3355 foreach my $include ( @includelist ) {
3357 my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3360 if ( length( $conf->config($inc_file, $agentnum) ) ) {
3362 @inc_src = $conf->config($inc_file, $agentnum);
3366 $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3368 my $convert_map = $convert_maps{$format}{$include};
3370 @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3371 s/--\@\]/$delimiters{$format}[1]/g;
3374 &$convert_map( $conf->config($inc_file, $agentnum) );
3378 my $inc_tt = new Text::Template (
3380 SOURCE => [ map "$_\n", @inc_src ],
3381 DELIMITERS => $delimiters{$format},
3382 ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3384 unless ( $inc_tt->compile() ) {
3385 my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3386 warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3390 $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3392 $invoice_data{$include} =~ s/\n+$//
3393 if ($format eq 'latex');
3398 foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3399 /invoice_lines\((\d*)\)/;
3400 $invoice_lines += $1 || scalar(@buf);
3403 die "no invoice_lines() functions in template?"
3404 if ( $format eq 'template' && !$wasfunc );
3406 if ($format eq 'template') {
3408 if ( $invoice_lines ) {
3409 $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3410 $invoice_data{'total_pages'}++
3411 if scalar(@buf) % $invoice_lines;
3414 #setup subroutine for the template
3415 $invoice_data{invoice_lines} = sub {
3416 my $lines = shift || scalar(@buf);
3428 push @collect, split("\n",
3429 $text_template->fill_in( HASH => \%invoice_data )
3431 $invoice_data{'page'}++;
3433 map "$_\n", @collect;
3435 # this is where we actually create the invoice
3436 warn "filling in template for invoice ". $self->invnum. "\n"
3438 warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3441 $text_template->fill_in(HASH => \%invoice_data);
3445 # helper routine for generating date ranges
3446 sub _prior_month30s {
3449 [ 1, 2592000 ], # 0-30 days ago
3450 [ 2592000, 5184000 ], # 30-60 days ago
3451 [ 5184000, 7776000 ], # 60-90 days ago
3452 [ 7776000, 0 ], # 90+ days ago
3455 map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3456 $_->[1] ? $self->_date - $_->[1] - 1 : '',
3461 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3463 Returns an postscript invoice, as a scalar.
3465 Options can be passed as a hashref (recommended) or as a list of time, template
3466 and then any key/value pairs for any other options.
3468 I<time> an optional value used to control the printing of overdue messages. The
3469 default is now. It isn't the date of the invoice; that's the `_date' field.
3470 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3471 L<Time::Local> and L<Date::Parse> for conversion functions.
3473 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3480 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3481 my $ps = generate_ps($file);
3483 unlink($barcodefile) if $barcodefile;
3488 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3490 Returns an PDF invoice, as a scalar.
3492 Options can be passed as a hashref (recommended) or as a list of time, template
3493 and then any key/value pairs for any other options.
3495 I<time> an optional value used to control the printing of overdue messages. The
3496 default is now. It isn't the date of the invoice; that's the `_date' field.
3497 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3498 L<Time::Local> and L<Date::Parse> for conversion functions.
3500 I<template>, if specified, is the name of a suffix for alternate invoices.
3502 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3509 my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3510 my $pdf = generate_pdf($file);
3512 unlink($barcodefile) if $barcodefile;
3517 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3519 Returns an HTML invoice, as a scalar.
3521 I<time> an optional value used to control the printing of overdue messages. The
3522 default is now. It isn't the date of the invoice; that's the `_date' field.
3523 It is specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
3524 L<Time::Local> and L<Date::Parse> for conversion functions.
3526 I<template>, if specified, is the name of a suffix for alternate invoices.
3528 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3530 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3531 when emailing the invoice as part of a multipart/related MIME email.
3539 %params = %{ shift() };
3541 $params{'time'} = shift;
3542 $params{'template'} = shift;
3543 $params{'cid'} = shift;
3546 $params{'format'} = 'html';
3548 $self->print_generic( %params );
3551 # quick subroutine for print_latex
3553 # There are ten characters that LaTeX treats as special characters, which
3554 # means that they do not simply typeset themselves:
3555 # # $ % & ~ _ ^ \ { }
3557 # TeX ignores blanks following an escaped character; if you want a blank (as
3558 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ...").
3562 $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3563 $value =~ s/([<>])/\$$1\$/g;
3569 encode_entities($value);
3573 sub _html_escape_nbsp {
3574 my $value = _html_escape(shift);
3575 $value =~ s/ +/ /g;
3579 #utility methods for print_*
3581 sub _translate_old_latex_format {
3582 warn "_translate_old_latex_format called\n"
3589 if ( $line =~ /^%%Detail\s*$/ ) {
3591 push @template, q![@--!,
3592 q! foreach my $_tr_line (@detail_items) {!,
3593 q! if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3594 q! $_tr_line->{'description'} .= !,
3595 q! "\\tabularnewline\n~~".!,
3596 q! join( "\\tabularnewline\n~~",!,
3597 q! @{$_tr_line->{'ext_description'}}!,
3601 while ( ( my $line_item_line = shift )
3602 !~ /^%%EndDetail\s*$/ ) {
3603 $line_item_line =~ s/'/\\'/g; # nice LTS
3604 $line_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3605 $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3606 push @template, " \$OUT .= '$line_item_line';";
3609 push @template, '}',
3612 } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3614 push @template, '[@--',
3615 ' foreach my $_tr_line (@total_items) {';
3617 while ( ( my $total_item_line = shift )
3618 !~ /^%%EndTotalDetails\s*$/ ) {
3619 $total_item_line =~ s/'/\\'/g; # nice LTS
3620 $total_item_line =~ s/\\/\\\\/g; # escape quotes and backslashes
3621 $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3622 push @template, " \$OUT .= '$total_item_line';";
3625 push @template, '}',
3629 $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3630 push @template, $line;
3636 warn "$_\n" foreach @template;
3644 my $conf = $self->conf;
3646 #check for an invoice-specific override
3647 return $self->invoice_terms if $self->invoice_terms;
3649 #check for a customer- specific override
3650 my $cust_main = $self->cust_main;
3651 return $cust_main->invoice_terms if $cust_main->invoice_terms;
3653 #use configured default
3654 $conf->config('invoice_default_terms') || '';
3660 if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3661 $duedate = $self->_date() + ( $1 * 86400 );
3668 $self->due_date ? time2str(shift, $self->due_date) : '';
3671 sub balance_due_msg {
3673 my $msg = $self->mt('Balance Due');
3674 return $msg unless $self->terms;
3675 if ( $self->due_date ) {
3676 $msg .= ' - ' . $self->mt('Please pay by'). ' '.
3677 $self->due_date2str($date_format);
3678 } elsif ( $self->terms ) {
3679 $msg .= ' - '. $self->terms;
3684 sub balance_due_date {
3686 my $conf = $self->conf;
3688 if ( $conf->exists('invoice_default_terms')
3689 && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3690 $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3695 sub credit_balance_msg {
3697 $self->mt('Credit Balance Remaining')
3700 =item invnum_date_pretty
3702 Returns a string with the invoice number and date, for example:
3703 "Invoice #54 (3/20/2008)"
3707 sub invnum_date_pretty {
3709 $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
3714 Returns a string with the date, for example: "3/20/2008"
3720 time2str($date_format, $self->_date);
3723 =item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
3725 Generate section information for all items appearing on this invoice.
3726 This will only be called for multi-section invoices.
3728 For each line item (L<FS::cust_bill_pkg> record), this will fetch all
3729 related display records (L<FS::cust_bill_pkg_display>) and organize
3730 them into two groups ("early" and "late" according to whether they come
3731 before or after the total), then into sections. A subtotal is calculated
3734 Section descriptions are returned in sort weight order. Each consists
3735 of a hash containing:
3737 description: the package category name, escaped
3738 subtotal: the total charges in that section
3739 tax_section: a flag indicating that the section contains only tax charges
3740 summarized: same as tax_section, for some reason
3741 sort_weight: the package category's sort weight
3743 If 'condense' is set on the display record, it also contains everything
3744 returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
3745 coderefs to generate parts of the invoice. This is not advised.
3749 LATE: an arrayref to push the "late" section hashes onto. The "early"
3750 group is simply returned from the method.
3752 SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
3753 Turning this on has the following effects:
3754 - Ignores display items with the 'summary' flag.
3755 - Combines all items into the "early" group.
3756 - Creates sections for all non-disabled package categories, even if they
3757 have no charges on this invoice, as well as a section with no name.
3759 ESCAPE: an escape function to use for section titles.
3761 EXTRA_SECTIONS: an arrayref of additional sections to return after the
3762 sorted list. If there are any of these, section subtotals exclude
3765 FORMAT: 'latex', 'html', or 'template' (i.e. text). Not used, but
3766 passed through to C<_condense_section()>.
3770 use vars qw(%pkg_category_cache);
3771 sub _items_sections {
3774 my $summarypage = shift;
3776 my $extra_sections = shift;
3780 my %late_subtotal = ();
3783 foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3786 my $usage = $cust_bill_pkg->usage;
3788 foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3789 next if ( $display->summary && $summarypage );
3791 my $section = $display->section;
3792 my $type = $display->type;
3794 $not_tax{$section} = 1
3795 unless $cust_bill_pkg->pkgnum == 0;
3797 if ( $display->post_total && !$summarypage ) {
3798 if (! $type || $type eq 'S') {
3799 $late_subtotal{$section} += $cust_bill_pkg->setup
3800 if $cust_bill_pkg->setup != 0;
3804 $late_subtotal{$section} += $cust_bill_pkg->recur
3805 if $cust_bill_pkg->recur != 0;
3808 if ($type && $type eq 'R') {
3809 $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3810 if $cust_bill_pkg->recur != 0;
3813 if ($type && $type eq 'U') {
3814 $late_subtotal{$section} += $usage
3815 unless scalar(@$extra_sections);
3820 next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3822 if (! $type || $type eq 'S') {
3823 $subtotal{$section} += $cust_bill_pkg->setup
3824 if $cust_bill_pkg->setup != 0;
3828 $subtotal{$section} += $cust_bill_pkg->recur
3829 if $cust_bill_pkg->recur != 0;
3832 if ($type && $type eq 'R') {
3833 $subtotal{$section} += $cust_bill_pkg->recur - $usage
3834 if $cust_bill_pkg->recur != 0;
3837 if ($type && $type eq 'U') {
3838 $subtotal{$section} += $usage
3839 unless scalar(@$extra_sections);
3848 %pkg_category_cache = ();
3850 push @$late, map { { 'description' => &{$escape}($_),
3851 'subtotal' => $late_subtotal{$_},
3853 'sort_weight' => ( _pkg_category($_)
3854 ? _pkg_category($_)->weight
3857 ((_pkg_category($_) && _pkg_category($_)->condense)
3858 ? $self->_condense_section($format)
3862 sort _sectionsort keys %late_subtotal;
3865 if ( $summarypage ) {
3866 @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3867 map { $_->categoryname } qsearch('pkg_category', {});
3868 push @sections, '' if exists($subtotal{''});
3870 @sections = keys %subtotal;
3873 my @early = map { { 'description' => &{$escape}($_),
3874 'subtotal' => $subtotal{$_},
3875 'summarized' => $not_tax{$_} ? '' : 'Y',
3876 'tax_section' => $not_tax{$_} ? '' : 'Y',
3877 'sort_weight' => ( _pkg_category($_)
3878 ? _pkg_category($_)->weight
3881 ((_pkg_category($_) && _pkg_category($_)->condense)
3882 ? $self->_condense_section($format)
3887 push @early, @$extra_sections if $extra_sections;
3889 sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3893 #helper subs for above
3896 _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3900 my $categoryname = shift;
3901 $pkg_category_cache{$categoryname} ||=
3902 qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3905 my %condensed_format = (
3906 'label' => [ qw( Description Qty Amount ) ],
3908 sub { shift->{description} },
3909 sub { shift->{quantity} },
3910 sub { my($href, %opt) = @_;
3911 ($opt{dollar} || ''). $href->{amount};
3914 'align' => [ qw( l r r ) ],
3915 'span' => [ qw( 5 1 1 ) ], # unitprices?
3916 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
3919 sub _condense_section {
3920 my ( $self, $format ) = ( shift, shift );
3922 map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3923 qw( description_generator
3926 total_line_generator
3931 sub _condensed_generator_defaults {
3932 my ( $self, $format ) = ( shift, shift );
3933 return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3942 sub _condensed_header_generator {
3943 my ( $self, $format ) = ( shift, shift );
3945 my ( $f, $prefix, $suffix, $separator, $column ) =
3946 _condensed_generator_defaults($format);
3948 if ($format eq 'latex') {
3949 $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3950 $suffix = "\\\\\n\\hline";
3953 sub { my ($d,$a,$s,$w) = @_;
3954 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3956 } elsif ( $format eq 'html' ) {
3957 $prefix = '<th></th>';
3961 sub { my ($d,$a,$s,$w) = @_;
3962 return qq!<th align="$html_align{$a}">$d</th>!;
3970 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
3972 &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3975 $prefix. join($separator, @result). $suffix;
3980 sub _condensed_description_generator {
3981 my ( $self, $format ) = ( shift, shift );
3983 my ( $f, $prefix, $suffix, $separator, $column ) =
3984 _condensed_generator_defaults($format);
3986 my $money_char = '$';
3987 if ($format eq 'latex') {
3988 $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3990 $separator = " & \n";
3992 sub { my ($d,$a,$s,$w) = @_;
3993 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3995 $money_char = '\\dollar';
3996 }elsif ( $format eq 'html' ) {
3997 $prefix = '"><td align="center"></td>';
4001 sub { my ($d,$a,$s,$w) = @_;
4002 return qq!<td align="$html_align{$a}">$d</td>!;
4004 #$money_char = $conf->config('money_char') || '$';
4005 $money_char = ''; # this is madness
4013 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4015 $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
4017 &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
4018 map { $f->{$_}->[$i] } qw(align span width)
4022 $prefix. join( $separator, @result ). $suffix;
4027 sub _condensed_total_generator {
4028 my ( $self, $format ) = ( shift, shift );
4030 my ( $f, $prefix, $suffix, $separator, $column ) =
4031 _condensed_generator_defaults($format);
4034 if ($format eq 'latex') {
4037 $separator = " & \n";
4039 sub { my ($d,$a,$s,$w) = @_;
4040 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4042 }elsif ( $format eq 'html' ) {
4046 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4048 sub { my ($d,$a,$s,$w) = @_;
4049 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4058 # my $r = &{$f->{fields}->[$i]}(@args);
4059 # $r .= ' Total' unless $i;
4061 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4063 &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
4064 map { $f->{$_}->[$i] } qw(align span width)
4068 $prefix. join( $separator, @result ). $suffix;
4073 =item total_line_generator FORMAT
4075 Returns a coderef used for generation of invoice total line items for this
4076 usage_class. FORMAT is either html or latex
4080 # should not be used: will have issues with hash element names (description vs
4081 # total_item and amount vs total_amount -- another array of functions?
4083 sub _condensed_total_line_generator {
4084 my ( $self, $format ) = ( shift, shift );
4086 my ( $f, $prefix, $suffix, $separator, $column ) =
4087 _condensed_generator_defaults($format);
4090 if ($format eq 'latex') {
4093 $separator = " & \n";
4095 sub { my ($d,$a,$s,$w) = @_;
4096 return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
4098 }elsif ( $format eq 'html' ) {
4102 $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
4104 sub { my ($d,$a,$s,$w) = @_;
4105 return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
4114 foreach (my $i = 0; $f->{label}->[$i]; $i++) {
4116 &{$column}( &{$f->{fields}->[$i]}(@args),
4117 map { $f->{$_}->[$i] } qw(align span width)
4121 $prefix. join( $separator, @result ). $suffix;
4126 #sub _items_extra_usage_sections {
4128 # my $escape = shift;
4130 # my %sections = ();
4132 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
4133 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
4135 # next unless $cust_bill_pkg->pkgnum > 0;
4137 # foreach my $section ( keys %usage_class ) {
4139 # my $usage = $cust_bill_pkg->usage($section);
4141 # next unless $usage && $usage > 0;
4143 # $sections{$section} ||= 0;
4144 # $sections{$section} += $usage;
4150 # map { { 'description' => &{$escape}($_),
4151 # 'subtotal' => $sections{$_},
4152 # 'summarized' => '',
4153 # 'tax_section' => '',
4156 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
4160 sub _items_extra_usage_sections {
4162 my $conf = $self->conf;
4170 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4172 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4173 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4174 next unless $cust_bill_pkg->pkgnum > 0;
4176 foreach my $classnum ( keys %usage_class ) {
4177 my $section = $usage_class{$classnum}->classname;
4178 $classnums{$section} = $classnum;
4180 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
4181 my $amount = $detail->amount;
4182 next unless $amount && $amount > 0;
4184 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
4185 $sections{$section}{amount} += $amount; #subtotal
4186 $sections{$section}{calls}++;
4187 $sections{$section}{duration} += $detail->duration;
4189 my $desc = $detail->regionname;
4190 my $description = $desc;
4191 $description = substr($desc, 0, $maxlength). '...'
4192 if $format eq 'latex' && length($desc) > $maxlength;
4194 $lines{$section}{$desc} ||= {
4195 description => &{$escape}($description),
4196 #pkgpart => $part_pkg->pkgpart,
4197 pkgnum => $cust_bill_pkg->pkgnum,
4202 #unit_amount => $cust_bill_pkg->unitrecur,
4203 quantity => $cust_bill_pkg->quantity,
4204 product_code => 'N/A',
4205 ext_description => [],
4208 $lines{$section}{$desc}{amount} += $amount;
4209 $lines{$section}{$desc}{calls}++;
4210 $lines{$section}{$desc}{duration} += $detail->duration;
4216 my %sectionmap = ();
4217 foreach (keys %sections) {
4218 my $usage_class = $usage_class{$classnums{$_}};
4219 $sectionmap{$_} = { 'description' => &{$escape}($_),
4220 'amount' => $sections{$_}{amount}, #subtotal
4221 'calls' => $sections{$_}{calls},
4222 'duration' => $sections{$_}{duration},
4224 'tax_section' => '',
4225 'sort_weight' => $usage_class->weight,
4226 ( $usage_class->format
4227 ? ( map { $_ => $usage_class->$_($format) }
4228 qw( description_generator header_generator total_generator total_line_generator )
4235 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4239 foreach my $section ( keys %lines ) {
4240 foreach my $line ( keys %{$lines{$section}} ) {
4241 my $l = $lines{$section}{$line};
4242 $l->{section} = $sectionmap{$section};
4243 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4244 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4249 return(\@sections, \@lines);
4255 my $end = $self->_date;
4257 # start at date of previous invoice + 1 second or 0 if no previous invoice
4258 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
4259 $start = 0 if !$start;
4262 my $cust_main = $self->cust_main;
4263 my @pkgs = $cust_main->all_pkgs;
4264 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4267 foreach my $pkg ( @pkgs ) {
4268 my @h_cust_svc = $pkg->h_cust_svc($end);
4269 foreach my $h_cust_svc ( @h_cust_svc ) {
4270 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4271 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4273 my $inserted = $h_cust_svc->date_inserted;
4274 my $deleted = $h_cust_svc->date_deleted;
4275 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
4277 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
4279 # DID either activated or ported in; cannot be both for same DID simultaneously
4280 if ($inserted >= $start && $inserted <= $end && $phone_inserted
4281 && (!$phone_inserted->lnp_status
4282 || $phone_inserted->lnp_status eq ''
4283 || $phone_inserted->lnp_status eq 'native')) {
4286 else { # this one not so clean, should probably move to (h_)svc_phone
4287 my $phone_portedin = qsearchs( 'h_svc_phone',
4288 { 'svcnum' => $h_cust_svc->svcnum,
4289 'lnp_status' => 'portedin' },
4290 FS::h_svc_phone->sql_h_searchs($end),
4292 $num_portedin++ if $phone_portedin;
4295 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4296 if($deleted >= $start && $deleted <= $end && $phone_deleted
4297 && (!$phone_deleted->lnp_status
4298 || $phone_deleted->lnp_status ne 'portingout')) {
4301 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
4302 && $phone_deleted->lnp_status
4303 && $phone_deleted->lnp_status eq 'portingout') {
4307 # increment usage minutes
4308 if ( $phone_inserted ) {
4309 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
4310 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
4313 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
4316 # don't look at this service again
4317 push @seen, $h_cust_svc->svcnum;
4321 $minutes = sprintf("%d", $minutes);
4322 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
4323 . "$num_deactivated Ported-Out: $num_portedout ",
4324 "Total Minutes: $minutes");
4327 sub _items_accountcode_cdr {
4332 my $section = { 'amount' => 0,
4335 'sort_weight' => '',
4337 'description' => 'Usage by Account Code',
4343 my %accountcodes = ();
4345 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4346 next unless $cust_bill_pkg->pkgnum > 0;
4348 my @header = $cust_bill_pkg->details_header;
4349 next unless scalar(@header);
4350 $section->{'header'} = join(',',@header);
4352 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4354 $section->{'header'} = $detail->formatted('format' => $format)
4355 if($detail->detail eq $section->{'header'});
4357 my $accountcode = $detail->accountcode;
4358 next unless $accountcode;
4360 my $amount = $detail->amount;
4361 next unless $amount && $amount > 0;
4363 $accountcodes{$accountcode} ||= {
4364 description => $accountcode,
4371 product_code => 'N/A',
4372 section => $section,
4373 ext_description => [ $section->{'header'} ],
4377 $section->{'amount'} += $amount;
4378 $accountcodes{$accountcode}{'amount'} += $amount;
4379 $accountcodes{$accountcode}{calls}++;
4380 $accountcodes{$accountcode}{duration} += $detail->duration;
4381 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
4385 foreach my $l ( values %accountcodes ) {
4386 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4387 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
4388 foreach my $sorted_detail ( @sorted_detail ) {
4389 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
4391 delete $l->{detail_temp};
4395 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
4397 return ($section,\@sorted_lines);
4400 sub _items_svc_phone_sections {
4402 my $conf = $self->conf;
4410 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4412 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4413 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4415 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4416 next unless $cust_bill_pkg->pkgnum > 0;
4418 my @header = $cust_bill_pkg->details_header;
4419 next unless scalar(@header);
4421 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4423 my $phonenum = $detail->phonenum;
4424 next unless $phonenum;
4426 my $amount = $detail->amount;
4427 next unless $amount && $amount > 0;
4429 $sections{$phonenum} ||= { 'amount' => 0,
4432 'sort_weight' => -1,
4433 'phonenum' => $phonenum,
4435 $sections{$phonenum}{amount} += $amount; #subtotal
4436 $sections{$phonenum}{calls}++;
4437 $sections{$phonenum}{duration} += $detail->duration;
4439 my $desc = $detail->regionname;
4440 my $description = $desc;
4441 $description = substr($desc, 0, $maxlength). '...'
4442 if $format eq 'latex' && length($desc) > $maxlength;
4444 $lines{$phonenum}{$desc} ||= {
4445 description => &{$escape}($description),
4446 #pkgpart => $part_pkg->pkgpart,
4454 product_code => 'N/A',
4455 ext_description => [],
4458 $lines{$phonenum}{$desc}{amount} += $amount;
4459 $lines{$phonenum}{$desc}{calls}++;
4460 $lines{$phonenum}{$desc}{duration} += $detail->duration;
4462 my $line = $usage_class{$detail->classnum}->classname;
4463 $sections{"$phonenum $line"} ||=
4467 'sort_weight' => $usage_class{$detail->classnum}->weight,
4468 'phonenum' => $phonenum,
4469 'header' => [ @header ],
4471 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
4472 $sections{"$phonenum $line"}{calls}++;
4473 $sections{"$phonenum $line"}{duration} += $detail->duration;
4475 $lines{"$phonenum $line"}{$desc} ||= {
4476 description => &{$escape}($description),
4477 #pkgpart => $part_pkg->pkgpart,
4485 product_code => 'N/A',
4486 ext_description => [],
4489 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4490 $lines{"$phonenum $line"}{$desc}{calls}++;
4491 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4492 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4493 $detail->formatted('format' => $format);
4498 my %sectionmap = ();
4499 my $simple = new FS::usage_class { format => 'simple' }; #bleh
4500 foreach ( keys %sections ) {
4501 my @header = @{ $sections{$_}{header} || [] };
4503 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4504 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4505 my $usage_class = $summary ? $simple : $usage_simple;
4506 my $ending = $summary ? ' usage charges' : '';
4509 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4511 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4512 'amount' => $sections{$_}{amount}, #subtotal
4513 'calls' => $sections{$_}{calls},
4514 'duration' => $sections{$_}{duration},
4516 'tax_section' => '',
4517 'phonenum' => $sections{$_}{phonenum},
4518 'sort_weight' => $sections{$_}{sort_weight},
4519 'post_total' => $summary, #inspire pagebreak
4521 ( map { $_ => $usage_class->$_($format, %gen_opt) }
4522 qw( description_generator
4525 total_line_generator
4532 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4533 $a->{sort_weight} <=> $b->{sort_weight}
4538 foreach my $section ( keys %lines ) {
4539 foreach my $line ( keys %{$lines{$section}} ) {
4540 my $l = $lines{$section}{$line};
4541 $l->{section} = $sectionmap{$section};
4542 $l->{amount} = sprintf( "%.2f", $l->{amount} );
4543 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4548 if($conf->exists('phone_usage_class_summary')) {
4549 # this only works with Latex
4553 # after this, we'll have only two sections per DID:
4554 # Calls Summary and Calls Detail
4555 foreach my $section ( @sections ) {
4556 if($section->{'post_total'}) {
4557 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4558 $section->{'total_line_generator'} = sub { '' };
4559 $section->{'total_generator'} = sub { '' };
4560 $section->{'header_generator'} = sub { '' };
4561 $section->{'description_generator'} = '';
4562 push @newsections, $section;
4563 my %calls_detail = %$section;
4564 $calls_detail{'post_total'} = '';
4565 $calls_detail{'sort_weight'} = '';
4566 $calls_detail{'description_generator'} = sub { '' };
4567 $calls_detail{'header_generator'} = sub {
4568 return ' & Date/Time & Called Number & Duration & Price'
4569 if $format eq 'latex';
4572 $calls_detail{'description'} = 'Calls Detail: '
4573 . $section->{'phonenum'};
4574 push @newsections, \%calls_detail;
4578 # after this, each usage class is collapsed/summarized into a single
4579 # line under the Calls Summary section
4580 foreach my $newsection ( @newsections ) {
4581 if($newsection->{'post_total'}) { # this means Calls Summary
4582 foreach my $section ( @sections ) {
4583 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
4584 && !$section->{'post_total'});
4585 my $newdesc = $section->{'description'};
4586 my $tn = $section->{'phonenum'};
4587 $newdesc =~ s/$tn//g;
4588 my $line = { ext_description => [],
4592 calls => $section->{'calls'},
4593 section => $newsection,
4594 duration => $section->{'duration'},
4595 description => $newdesc,
4596 amount => sprintf("%.2f",$section->{'amount'}),
4597 product_code => 'N/A',
4599 push @newlines, $line;
4604 # after this, Calls Details is populated with all CDRs
4605 foreach my $newsection ( @newsections ) {
4606 if(!$newsection->{'post_total'}) { # this means Calls Details
4607 foreach my $line ( @lines ) {
4608 next unless (scalar(@{$line->{'ext_description'}}) &&
4609 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4611 my @extdesc = @{$line->{'ext_description'}};
4613 foreach my $extdesc ( @extdesc ) {
4614 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4615 push @newextdesc, $extdesc;
4617 $line->{'ext_description'} = \@newextdesc;
4618 $line->{'section'} = $newsection;
4619 push @newlines, $line;
4624 return(\@newsections, \@newlines);
4627 return(\@sections, \@lines);
4631 sub _items { # seems to be unused
4634 #my @display = scalar(@_)
4636 # : qw( _items_previous _items_pkg );
4637 # #: qw( _items_pkg );
4638 # #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4639 my @display = qw( _items_previous _items_pkg );
4642 foreach my $display ( @display ) {
4643 push @b, $self->$display(@_);
4648 sub _items_previous {
4650 my $conf = $self->conf;
4651 my $cust_main = $self->cust_main;
4652 my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4654 foreach ( @pr_cust_bill ) {
4655 my $date = $conf->exists('invoice_show_prior_due_date')
4656 ? 'due '. $_->due_date2str($date_format)
4657 : time2str($date_format, $_->_date);
4659 'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
4660 #'pkgpart' => 'N/A',
4662 'amount' => sprintf("%.2f", $_->owed),
4668 # 'description' => 'Previous Balance',
4669 # #'pkgpart' => 'N/A',
4670 # 'pkgnum' => 'N/A',
4671 # 'amount' => sprintf("%10.2f", $pr_total ),
4672 # 'ext_description' => [ map {
4673 # "Invoice ". $_->invnum.
4674 # " (". time2str("%x",$_->_date). ") ".
4675 # sprintf("%10.2f", $_->owed)
4676 # } @pr_cust_bill ],
4681 =item _items_pkg [ OPTIONS ]
4683 Return line item hashes for each package item on this invoice. Nearly
4686 $self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
4688 The only OPTIONS accepted is 'section', which may point to a hashref
4689 with a key named 'condensed', which may have a true value. If it
4690 does, this method tries to merge identical items into items with
4691 'quantity' equal to the number of items (not the sum of their
4692 separate quantities, for some reason).
4700 warn "$me _items_pkg searching for all package line items\n"
4703 my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4705 warn "$me _items_pkg filtering line items\n"
4707 my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4709 if ($options{section} && $options{section}->{condensed}) {
4711 warn "$me _items_pkg condensing section\n"
4715 local $Storable::canonical = 1;
4716 foreach ( @items ) {
4718 delete $item->{ref};
4719 delete $item->{ext_description};
4720 my $key = freeze($item);
4721 $itemshash{$key} ||= 0;
4722 $itemshash{$key} ++; # += $item->{quantity};
4724 @items = sort { $a->{description} cmp $b->{description} }
4725 map { my $i = thaw($_);
4726 $i->{quantity} = $itemshash{$_};
4728 sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4734 warn "$me _items_pkg returning ". scalar(@items). " items\n"
4741 return 0 unless $a->itemdesc cmp $b->itemdesc;
4742 return -1 if $b->itemdesc eq 'Tax';
4743 return 1 if $a->itemdesc eq 'Tax';
4744 return -1 if $b->itemdesc eq 'Other surcharges';
4745 return 1 if $a->itemdesc eq 'Other surcharges';
4746 $a->itemdesc cmp $b->itemdesc;
4751 my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4752 $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4755 =item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
4757 Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
4758 list of hashrefs describing the line items they generate on the invoice.
4760 OPTIONS may include:
4762 format: the invoice format.
4764 escape_function: the function used to escape strings.
4766 format_function: the function used to format CDRs.
4768 section: a hashref containing 'description'; if this is present,
4769 cust_bill_pkg_display records not belonging to this section are
4772 multisection: a flag indicating that this is a multisection invoice,
4773 which does something complicated.
4775 multilocation: a flag to display the location label for the package.
4777 Returns a list of hashrefs, each of which may contain:
4779 pkgnum, description, amount, unit_amount, quantity, _is_setup, and
4780 ext_description, which is an arrayref of detail lines to show below
4785 sub _items_cust_bill_pkg {
4787 my $conf = $self->conf;
4788 my $cust_bill_pkgs = shift;
4791 my $format = $opt{format} || '';
4792 my $escape_function = $opt{escape_function} || sub { shift };
4793 my $format_function = $opt{format_function} || '';
4794 my $unsquelched = $opt{unsquelched} || ''; #unused
4795 my $section = $opt{section}->{description} if $opt{section};
4796 my $summary_page = $opt{summary_page} || ''; #unused
4797 my $multilocation = $opt{multilocation} || '';
4798 my $multisection = $opt{multisection} || '';
4799 my $discount_show_always = 0;
4801 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
4804 my ($s, $r, $u) = ( undef, undef, undef );
4805 foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4808 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4809 if ( $_ && !$cust_bill_pkg->hidden ) {
4810 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
4811 $_->{amount} =~ s/^\-0\.00$/0.00/;
4812 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4814 if $_->{amount} != 0
4815 || $discount_show_always
4816 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
4817 || ( $_->{_is_setup} && $_->{setup_show_zero} )
4823 warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
4824 $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
4827 foreach my $display ( grep { defined($section)
4828 ? $_->section eq $section
4831 #grep { !$_->summary || !$summary_page } # bunk!
4832 grep { !$_->summary || $multisection }
4833 $cust_bill_pkg->cust_bill_pkg_display
4837 warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
4838 $display->billpkgdisplaynum. "\n"
4841 my $type = $display->type;
4843 my $desc = $cust_bill_pkg->desc;
4844 $desc = substr($desc, 0, $maxlength). '...'
4845 if $format eq 'latex' && length($desc) > $maxlength;
4847 my %details_opt = ( 'format' => $format,
4848 'escape_function' => $escape_function,
4849 'format_function' => $format_function,
4852 if ( $cust_bill_pkg->pkgnum > 0 ) {
4854 warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4857 my $cust_pkg = $cust_bill_pkg->cust_pkg;
4859 # start/end dates for invoice formats that do nonstandard
4861 my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
4863 if ( (!$type || $type eq 'S')
4864 && ( $cust_bill_pkg->setup != 0
4865 || $cust_bill_pkg->setup_show_zero
4870 warn "$me _items_cust_bill_pkg adding setup\n"
4873 my $description = $desc;
4874 $description .= ' Setup'
4875 if $cust_bill_pkg->recur != 0
4876 || $discount_show_always
4877 || $cust_bill_pkg->recur_show_zero;
4880 unless ( $cust_pkg->part_pkg->hide_svc_detail
4881 || $cust_bill_pkg->hidden )
4884 push @d, map &{$escape_function}($_),
4885 $cust_pkg->h_labels_short($self->_date, undef, 'I')
4886 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4888 if ( $multilocation ) {
4889 my $loc = $cust_pkg->location_label;
4890 $loc = substr($loc, 0, $maxlength). '...'
4891 if $format eq 'latex' && length($loc) > $maxlength;
4892 push @d, &{$escape_function}($loc);
4895 } #unless hiding service details
4897 push @d, $cust_bill_pkg->details(%details_opt)
4898 if $cust_bill_pkg->recur == 0;
4900 if ( $cust_bill_pkg->hidden ) {
4901 $s->{amount} += $cust_bill_pkg->setup;
4902 $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4903 push @{ $s->{ext_description} }, @d;
4907 description => $description,
4908 #pkgpart => $part_pkg->pkgpart,
4909 pkgnum => $cust_bill_pkg->pkgnum,
4910 amount => $cust_bill_pkg->setup,
4911 setup_show_zero => $cust_bill_pkg->setup_show_zero,
4912 unit_amount => $cust_bill_pkg->unitsetup,
4913 quantity => $cust_bill_pkg->quantity,
4914 ext_description => \@d,
4920 if ( ( !$type || $type eq 'R' || $type eq 'U' )
4922 $cust_bill_pkg->recur != 0
4923 || $cust_bill_pkg->setup == 0
4924 || $discount_show_always
4925 || $cust_bill_pkg->recur_show_zero
4930 warn "$me _items_cust_bill_pkg adding recur/usage\n"
4933 my $is_summary = $display->summary;
4934 my $description = ($is_summary && $type && $type eq 'U')
4935 ? "Usage charges" : $desc;
4937 $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4938 " - ". time2str($date_format, $cust_bill_pkg->edate).
4940 unless $conf->exists('disable_line_item_date_ranges')
4941 || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1);
4944 my @seconds = (); # for display of usage info
4946 #at least until cust_bill_pkg has "past" ranges in addition to
4947 #the "future" sdate/edate ones... see #3032
4948 my @dates = ( $self->_date );
4949 my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4950 push @dates, $prev->sdate if $prev;
4951 push @dates, undef if !$prev;
4953 unless ( $cust_pkg->part_pkg->hide_svc_detail
4954 || $cust_bill_pkg->itemdesc
4955 || $cust_bill_pkg->hidden
4956 || $is_summary && $type && $type eq 'U' )
4959 warn "$me _items_cust_bill_pkg adding service details\n"
4962 push @d, map &{$escape_function}($_),
4963 $cust_pkg->h_labels_short(@dates, 'I')
4964 #$cust_bill_pkg->edate,
4965 #$cust_bill_pkg->sdate)
4966 unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4968 warn "$me _items_cust_bill_pkg done adding service details\n"
4971 if ( $multilocation ) {
4972 my $loc = $cust_pkg->location_label;
4973 $loc = substr($loc, 0, $maxlength). '...'
4974 if $format eq 'latex' && length($loc) > $maxlength;
4975 push @d, &{$escape_function}($loc);
4978 # Display of seconds_since_sqlradacct:
4979 # On the invoice, when processing @detail_items, look for a field
4980 # named 'seconds'. This will contain total seconds for each
4981 # service, in the same order as @ext_description. For services
4982 # that don't support this it will show undef.
4983 if ( $conf->exists('svc_acct-usage_seconds')
4984 and ! $cust_bill_pkg->pkgpart_override ) {
4985 foreach my $cust_svc (
4986 $cust_pkg->h_cust_svc(@dates, 'I')
4989 # eval because not having any part_export_usage exports
4990 # is a fatal error, last_bill/_date because that's how
4991 # sqlradius_hour billing does it
4993 $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
4995 push @seconds, $sec;
4997 } #if svc_acct-usage_seconds
5001 unless ( $is_summary ) {
5002 warn "$me _items_cust_bill_pkg adding details\n"
5005 #instead of omitting details entirely in this case (unwanted side
5006 # effects), just omit CDRs
5007 $details_opt{'format_function'} = sub { () }
5008 if $type && $type eq 'R';
5010 push @d, $cust_bill_pkg->details(%details_opt);
5013 warn "$me _items_cust_bill_pkg calculating amount\n"
5018 $amount = $cust_bill_pkg->recur;
5019 } elsif ($type eq 'R') {
5020 $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
5021 } elsif ($type eq 'U') {
5022 $amount = $cust_bill_pkg->usage;
5025 if ( !$type || $type eq 'R' ) {
5027 warn "$me _items_cust_bill_pkg adding recur\n"
5030 if ( $cust_bill_pkg->hidden ) {
5031 $r->{amount} += $amount;
5032 $r->{unit_amount} += $cust_bill_pkg->unitrecur;
5033 push @{ $r->{ext_description} }, @d;
5036 description => $description,
5037 #pkgpart => $part_pkg->pkgpart,
5038 pkgnum => $cust_bill_pkg->pkgnum,
5040 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5041 unit_amount => $cust_bill_pkg->unitrecur,
5042 quantity => $cust_bill_pkg->quantity,
5044 ext_description => \@d,
5046 $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
5049 } else { # $type eq 'U'
5051 warn "$me _items_cust_bill_pkg adding usage\n"
5054 if ( $cust_bill_pkg->hidden ) {
5055 $u->{amount} += $amount;
5056 $u->{unit_amount} += $cust_bill_pkg->unitrecur;
5057 push @{ $u->{ext_description} }, @d;
5060 description => $description,
5061 #pkgpart => $part_pkg->pkgpart,
5062 pkgnum => $cust_bill_pkg->pkgnum,
5064 recur_show_zero => $cust_bill_pkg->recur_show_zero,
5065 unit_amount => $cust_bill_pkg->unitrecur,
5066 quantity => $cust_bill_pkg->quantity,
5068 ext_description => \@d,
5073 } # recurring or usage with recurring charge
5075 } else { #pkgnum tax or one-shot line item (??)
5077 warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
5080 if ( $cust_bill_pkg->setup != 0 ) {
5082 'description' => $desc,
5083 'amount' => sprintf("%.2f", $cust_bill_pkg->setup),
5086 if ( $cust_bill_pkg->recur != 0 ) {
5088 'description' => "$desc (".
5089 time2str($date_format, $cust_bill_pkg->sdate). ' - '.
5090 time2str($date_format, $cust_bill_pkg->edate). ')',
5091 'amount' => sprintf("%.2f", $cust_bill_pkg->recur),
5099 $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
5100 && $conf->exists('discount-show-always'));
5104 foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
5106 $_->{amount} = sprintf( "%.2f", $_->{amount} ),
5107 $_->{amount} =~ s/^\-0\.00$/0.00/;
5108 $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
5110 if $_->{amount} != 0
5111 || $discount_show_always
5112 || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
5113 || ( $_->{_is_setup} && $_->{setup_show_zero} )
5117 warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
5124 sub _items_credits {
5125 my( $self, %opt ) = @_;
5126 my $trim_len = $opt{'trim_len'} || 60;
5130 foreach ( $self->cust_credited ) {
5132 #something more elaborate if $_->amount ne $_->cust_credit->credited ?
5134 my $reason = substr($_->cust_credit->reason, 0, $trim_len);
5135 $reason .= '...' if length($reason) < length($_->cust_credit->reason);
5136 $reason = " ($reason) " if $reason;
5139 #'description' => 'Credit ref\#'. $_->crednum.
5140 # " (". time2str("%x",$_->cust_credit->_date) .")".
5142 'description' => $self->mt('Credit applied').' '.
5143 time2str($date_format,$_->cust_credit->_date). $reason,
5144 'amount' => sprintf("%.2f",$_->amount),
5152 sub _items_payments {
5156 #get & print payments
5157 foreach ( $self->cust_bill_pay ) {
5159 #something more elaborate if $_->amount ne ->cust_pay->paid ?
5162 'description' => $self->mt('Payment received').' '.
5163 time2str($date_format,$_->cust_pay->_date ),
5164 'amount' => sprintf("%.2f", $_->amount )
5172 =item _items_discounts_avail
5174 Returns an array of line item hashrefs representing available term discounts
5175 for this invoice. This makes the same assumptions that apply to term
5176 discounts in general: that the package is billed monthly, at a flat rate,
5177 with no usage charges. A prorated first month will be handled, as will
5178 a setup fee if the discount is allowed to apply to setup fees.
5182 sub _items_discounts_avail {
5185 my $list_pkgnums = 0; # if any packages are not eligible for all discounts
5187 my ($previous_balance) = $self->previous;
5189 foreach (qsearch('discount',{ 'months' => { op => '>', value => 1} })) {
5190 $terms{$_->months} = {
5192 base => $previous_balance || 0, # pre-discount sum of charges
5193 discounted => $previous_balance || 0, # post-discount sum
5194 list_pkgnums => 0, # whether any packages are not discounted
5197 foreach my $months (keys %terms) {
5198 my $hash = $terms{$months};
5200 # tricky, because packages may not all be eligible for the same discounts
5201 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
5202 my $cust_pkg = $cust_bill_pkg->cust_pkg or next;
5203 my $part_pkg = $cust_pkg->part_pkg or next;
5205 next if $part_pkg->freq ne '1';
5206 my $setup = $cust_bill_pkg->setup || 0;
5207 my $recur = $cust_bill_pkg->recur || 0;
5208 my $permonth = $part_pkg->base_recur_permonth || 0;
5210 my ($discount) = grep { $_->months == $months }
5211 map { $_->discount } $part_pkg->part_pkg_discount;
5213 $hash->{base} += $setup + $recur + ($months - 1) * $permonth;
5217 if ( $discount->setup ) {
5218 $discountable += $setup;
5221 $hash->{discounted} += $setup;
5224 if ( $discount->percent ) {
5225 $discountable += $months * $permonth;
5226 $discountable -= ($discountable * $discount->percent / 100);
5227 $discountable -= ($permonth - $recur); # correct for prorate
5228 $hash->{discounted} += $discountable;
5231 $discountable += $recur;
5232 $discountable -= $discount->amount * $recur/$permonth;
5234 $discountable += ($months - 1) * max($permonth - $discount->amount,0);
5237 $hash->{discounted} += $discountable;
5238 push @{ $hash->{pkgnums} }, $cust_pkg->pkgnum;
5241 $hash->{discounted} += $setup + $recur + ($months - 1) * $permonth;
5242 $hash->{list_pkgnums} = 1;
5244 } #foreach $cust_bill_pkg
5246 # don't show this line if no packages have discounts at this term
5247 # or if there are no new charges to apply the discount to
5248 delete $terms{$months} if $hash->{base} == $hash->{discounted}
5249 or $hash->{base} == 0;
5253 $list_pkgnums = grep { $_->{list_pkgnums} > 0 } values %terms;
5255 foreach my $months (keys %terms) {
5256 my $hash = $terms{$months};
5257 my $term_total = sprintf('%.2f', $hash->{discounted});
5258 # possibly shouldn't include previous balance in these?
5259 my $percent = sprintf('%.0f', 100 * (1 - $term_total / $hash->{base}) );
5260 my $permonth = sprintf('%.2f', $term_total / $months);
5262 $hash->{description} = $self->mt('Save [_1]% by paying for [_2] months',
5265 $hash->{amount} = $self->mt('[_1] ([_2] per month)',
5266 $term_total, $money_char.$permonth
5270 if ( $list_pkgnums ) {
5271 push @detail, $self->mt('discount on item'). ' '.
5272 join(', ', map { "#$_" } @{ $hash->{pkgnums} });
5274 $hash->{ext_description} = join ', ', @detail;
5277 map { $terms{$_} } sort {$b <=> $a} keys %terms;
5280 =item call_details [ OPTION => VALUE ... ]
5282 Returns an array of CSV strings representing the call details for this invoice
5283 The only option available is the boolean prepend_billed_number
5288 my ($self, %opt) = @_;
5290 my $format_function = sub { shift };
5292 if ($opt{prepend_billed_number}) {
5293 $format_function = sub {
5297 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
5302 my @details = map { $_->details( 'format_function' => $format_function,
5303 'escape_function' => sub{ return() },
5307 $self->cust_bill_pkg;
5308 my $header = $details[0];
5309 ( $header, grep { $_ ne $header } @details );
5319 =item process_reprint
5323 sub process_reprint {
5324 process_re_X('print', @_);
5327 =item process_reemail
5331 sub process_reemail {
5332 process_re_X('email', @_);
5340 process_re_X('fax', @_);
5348 process_re_X('ftp', @_);
5355 sub process_respool {
5356 process_re_X('spool', @_);
5359 use Storable qw(thaw);
5363 my( $method, $job ) = ( shift, shift );
5364 warn "$me process_re_X $method for job $job\n" if $DEBUG;
5366 my $param = thaw(decode_base64(shift));
5367 warn Dumper($param) if $DEBUG;
5378 my($method, $job, %param ) = @_;
5380 warn "re_X $method for job $job with param:\n".
5381 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
5384 #some false laziness w/search/cust_bill.html
5386 my $orderby = 'ORDER BY cust_bill._date';
5388 my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
5390 my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
5392 my @cust_bill = qsearch( {
5393 #'select' => "cust_bill.*",
5394 'table' => 'cust_bill',
5395 'addl_from' => $addl_from,
5397 'extra_sql' => $extra_sql,
5398 'order_by' => $orderby,
5402 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
5404 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
5407 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
5408 foreach my $cust_bill ( @cust_bill ) {
5409 $cust_bill->$method();
5411 if ( $job ) { #progressbar foo
5413 if ( time - $min_sec > $last ) {
5414 my $error = $job->update_statustext(
5415 int( 100 * $num / scalar(@cust_bill) )
5417 die $error if $error;
5428 =head1 CLASS METHODS
5434 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
5439 my ($class, $start, $end) = @_;
5441 $class->paid_sql($start, $end). ' - '.
5442 $class->credited_sql($start, $end);
5447 Returns an SQL fragment to retreive the net amount (charged minus credited).
5452 my ($class, $start, $end) = @_;
5453 'charged - '. $class->credited_sql($start, $end);
5458 Returns an SQL fragment to retreive the amount paid against this invoice.
5463 my ($class, $start, $end) = @_;
5464 $start &&= "AND cust_bill_pay._date <= $start";
5465 $end &&= "AND cust_bill_pay._date > $end";
5466 $start = '' unless defined($start);
5467 $end = '' unless defined($end);
5468 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
5469 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
5474 Returns an SQL fragment to retreive the amount credited against this invoice.
5479 my ($class, $start, $end) = @_;
5480 $start &&= "AND cust_credit_bill._date <= $start";
5481 $end &&= "AND cust_credit_bill._date > $end";
5482 $start = '' unless defined($start);
5483 $end = '' unless defined($end);
5484 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
5485 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
5490 Returns an SQL fragment to retrieve the due date of an invoice.
5491 Currently only supported on PostgreSQL.
5496 my $conf = new FS::Conf;
5500 cust_bill.invoice_terms,
5501 cust_main.invoice_terms,
5502 \''.($conf->config('invoice_default_terms') || '').'\'
5503 ), E\'Net (\\\\d+)\'
5505 ) * 86400 + cust_bill._date'
5508 =item search_sql_where HASHREF
5510 Class method which returns an SQL WHERE fragment to search for parameters
5511 specified in HASHREF. Valid parameters are
5517 List reference of start date, end date, as UNIX timestamps.
5527 List reference of charged limits (exclusive).
5531 List reference of charged limits (exclusive).
5535 flag, return open invoices only
5539 flag, return net invoices only
5543 =item newest_percust
5547 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5551 sub search_sql_where {
5552 my($class, $param) = @_;
5554 warn "$me search_sql_where called with params: \n".
5555 join("\n", map { " $_: ". $param->{$_} } keys %$param ). "\n";
5561 if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5562 push @search, "cust_main.agentnum = $1";
5566 if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
5567 push @search, "cust_bill.custnum = $1";
5571 if ( $param->{_date} ) {
5572 my($beginning, $ending) = @{$param->{_date}};
5574 push @search, "cust_bill._date >= $beginning",
5575 "cust_bill._date < $ending";
5579 if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5580 push @search, "cust_bill.invnum >= $1";
5582 if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5583 push @search, "cust_bill.invnum <= $1";
5587 if ( $param->{charged} ) {
5588 my @charged = ref($param->{charged})
5589 ? @{ $param->{charged} }
5590 : ($param->{charged});
5592 push @search, map { s/^charged/cust_bill.charged/; $_; }
5596 my $owed_sql = FS::cust_bill->owed_sql;
5599 if ( $param->{owed} ) {
5600 my @owed = ref($param->{owed})
5601 ? @{ $param->{owed} }
5603 push @search, map { s/^owed/$owed_sql/; $_; }
5608 push @search, "0 != $owed_sql"
5609 if $param->{'open'};
5610 push @search, '0 != '. FS::cust_bill->net_sql
5614 push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5615 if $param->{'days'};
5618 if ( $param->{'newest_percust'} ) {
5620 #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5621 #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5623 my @newest_where = map { my $x = $_;
5624 $x =~ s/\bcust_bill\./newest_cust_bill./g;
5627 grep ! /^cust_main./, @search;
5628 my $newest_where = scalar(@newest_where)
5629 ? ' AND '. join(' AND ', @newest_where)
5633 push @search, "cust_bill._date = (
5634 SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5635 WHERE newest_cust_bill.custnum = cust_bill.custnum
5641 #agent virtualization
5642 my $curuser = $FS::CurrentUser::CurrentUser;
5643 if ( $curuser->username eq 'fs_queue'
5644 && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5646 my $newuser = qsearchs('access_user', {
5647 'username' => $username,
5651 $curuser = $newuser;
5653 warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5656 push @search, $curuser->agentnums_sql;
5658 join(' AND ', @search );
5670 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5671 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base