2 use base qw( FS::cust_bill::Search FS::Template_Mixin
3 FS::cust_main_Mixin FS::Record
7 use vars qw( $DEBUG $me );
10 use Fcntl qw(:flock); #for spool_csv
12 use List::Util qw(min max sum);
17 use Storable qw( freeze thaw );
19 use FS::UID qw( datasrc );
20 use FS::Misc qw( send_fax do_print );
21 use FS::Record qw( qsearch qsearchs dbh );
22 use FS::cust_statement;
23 use FS::cust_bill_pkg;
24 use FS::cust_bill_pkg_display;
25 use FS::cust_bill_pkg_detail;
29 use FS::cust_credit_bill;
33 use FS::cust_bill_pay;
36 use FS::cust_bill_batch;
37 use FS::cust_bill_pay_pkg;
38 use FS::cust_credit_bill_pkg;
39 use FS::discount_plan;
40 use FS::cust_bill_void;
46 $me = '[FS::cust_bill]';
50 FS::cust_bill - Object methods for cust_bill records
56 $record = new FS::cust_bill \%hash;
57 $record = new FS::cust_bill { 'column' => 'value' };
59 $error = $record->insert;
61 $error = $new_record->replace($old_record);
63 $error = $record->delete;
65 $error = $record->check;
67 ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
69 @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
71 ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
73 @cust_pay_objects = $cust_bill->cust_pay;
75 $tax_amount = $record->tax;
77 @lines = $cust_bill->print_text;
78 @lines = $cust_bill->print_text('time' => $time);
82 An FS::cust_bill object represents an invoice; a declaration that a customer
83 owes you money. The specific charges are itemized as B<cust_bill_pkg> records
84 (see L<FS::cust_bill_pkg>). FS::cust_bill inherits from FS::Record. The
85 following fields are currently supported:
91 =item invnum - primary key (assigned automatically for new invoices)
93 =item custnum - customer (see L<FS::cust_main>)
95 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
96 L<Time::Local> and L<Date::Parse> for conversion functions.
98 =item charged - amount of this invoice
100 =item invoice_terms - optional terms override for this specific invoice
108 =item billing_balance - the customer's balance immediately before generating
109 this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
110 to determine the customer's balance at a specific time.
112 =item previous_balance - the customer's balance immediately after generating
113 the invoice before this one. DEPRECATED.
115 =item printed - formerly used to track the number of times an invoice had
116 been printed; no longer used.
124 =item closed - books closed flag, empty or `Y'
126 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
128 =item agent_invid - legacy invoice number
130 =item promised_date - customer promised payment date, for collection
132 =item pending - invoice is still being generated, empty or 'Y'
142 Creates a new invoice. To add the invoice to the database, see L<"insert">.
143 Invoices are normally created by calling the bill method of a customer object
144 (see L<FS::cust_main>).
148 sub table { 'cust_bill'; }
149 sub template_conf { 'invoice_'; }
151 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
152 # (except email_subject and invnum_date_pretty)
155 $self->conf->config('notice_name') || 'Invoice'
158 sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
159 sub cust_unlinked_msg {
161 "WARNING: can't find cust_main.custnum ". $self->custnum.
162 ' (cust_bill.invnum '. $self->invnum. ')';
167 Adds this invoice to the database ("Posts" the invoice). If there is an error,
168 returns the error, otherwise returns false.
174 warn "$me insert called\n" if $DEBUG;
176 local $SIG{HUP} = 'IGNORE';
177 local $SIG{INT} = 'IGNORE';
178 local $SIG{QUIT} = 'IGNORE';
179 local $SIG{TERM} = 'IGNORE';
180 local $SIG{TSTP} = 'IGNORE';
181 local $SIG{PIPE} = 'IGNORE';
183 my $oldAutoCommit = $FS::UID::AutoCommit;
184 local $FS::UID::AutoCommit = 0;
187 my $error = $self->SUPER::insert;
189 $dbh->rollback if $oldAutoCommit;
193 if ( $self->get('cust_bill_pkg') ) {
194 foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
195 $cust_bill_pkg->invnum($self->invnum);
196 my $error = $cust_bill_pkg->insert;
198 $dbh->rollback if $oldAutoCommit;
199 return "can't create invoice line item: $error";
204 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
209 =item void [ REASON [ , REPROCESS_CDRS ] ]
211 Voids this invoice: deletes the invoice and adds a record of the voided invoice
212 to the FS::cust_bill_void table (and related tables starting from
213 FS::cust_bill_pkg_void).
219 my $reason = scalar(@_) ? shift : '';
220 my $reprocess_cdrs = scalar(@_) ? shift : '';
222 unless (ref($reason) || !$reason) {
223 $reason = FS::reason->new_or_existing(
225 'type' => 'Invoice void',
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 my $cust_bill_void = new FS::cust_bill_void ( {
242 map { $_ => $self->get($_) } $self->fields
244 $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
245 my $error = $cust_bill_void->insert;
247 $dbh->rollback if $oldAutoCommit;
251 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
252 my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
254 $dbh->rollback if $oldAutoCommit;
259 $error = $self->_delete;
261 $dbh->rollback if $oldAutoCommit;
265 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
271 # removed docs entirely and renamed method to _delete to further indicate it is
272 # internal-only and discourage use
276 # DO NOT USE THIS METHOD. Instead, apply a credit against the invoice, or use
277 # the B<void> method.
279 # This is only for internal use by V<void>, which is what you should be using.
281 # DO NOT USE THIS METHOD. Whatever reason you think you have is almost certainly
282 # wrong. Use B<void>, that's what it is for. Really. This means you.
288 return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
290 local $SIG{HUP} = 'IGNORE';
291 local $SIG{INT} = 'IGNORE';
292 local $SIG{QUIT} = 'IGNORE';
293 local $SIG{TERM} = 'IGNORE';
294 local $SIG{TSTP} = 'IGNORE';
295 local $SIG{PIPE} = 'IGNORE';
297 my $oldAutoCommit = $FS::UID::AutoCommit;
298 local $FS::UID::AutoCommit = 0;
301 foreach my $table (qw(
308 #cust_event # problematic
309 #cust_pay_batch # unnecessary
311 foreach my $linked ( $self->$table() ) {
312 my $error = $linked->delete;
314 $dbh->rollback if $oldAutoCommit;
321 my $error = $self->SUPER::delete(@_);
323 $dbh->rollback if $oldAutoCommit;
327 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
333 =item replace [ OLD_RECORD ]
335 You can, but probably shouldn't modify invoices...
337 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
338 supplied, replaces this record. If there is an error, returns the error,
339 otherwise returns false.
343 #replace can be inherited from Record.pm
345 # replace_check is now the preferred way to #implement replace data checks
346 # (so $object->replace() works without an argument)
349 my( $new, $old ) = ( shift, shift );
350 return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
351 #return "Can't change _date!" unless $old->_date eq $new->_date;
352 return "Can't change _date" unless $old->_date == $new->_date;
353 return "Can't change charged" unless $old->charged == $new->charged
354 || $old->pending eq 'Y'
355 || $old->charged == 0
356 || $new->{'Hash'}{'cc_surcharge_replace_hack'};
362 =item add_cc_surcharge
368 sub add_cc_surcharge {
369 my ($self, $pkgnum, $amount) = (shift, shift, shift);
372 my $cust_bill_pkg = new FS::cust_bill_pkg({
373 'invnum' => $self->invnum,
377 $error = $cust_bill_pkg->insert;
378 return $error if $error;
380 $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
381 $self->charged($self->charged+$amount);
382 $error = $self->replace;
383 return $error if $error;
385 $self->apply_payments_and_credits;
391 Checks all fields to make sure this is a valid invoice. If there is an error,
392 returns the error, otherwise returns false. Called by the insert and replace
401 $self->ut_numbern('invnum')
402 || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
403 || $self->ut_numbern('_date')
404 || $self->ut_money('charged')
405 || $self->ut_numbern('printed')
406 || $self->ut_enum('closed', [ '', 'Y' ])
407 || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
408 || $self->ut_numbern('agent_invid') #varchar?
409 || $self->ut_flag('pending')
411 return $error if $error;
413 $self->_date(time) unless $self->_date;
415 $self->printed(0) if $self->printed eq '';
422 Returns the displayed invoice number for this invoice: agent_invid if
423 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
429 if ( $self->agent_invid
430 && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
431 return $self->agent_invid;
433 return $self->invnum;
439 Returns the customer's last invoice before this one.
445 if ( !$self->get('previous_bill') ) {
446 $self->set('previous_bill', qsearchs({
447 'table' => 'cust_bill',
448 'hashref' => { 'custnum' => $self->custnum,
449 '_date' => { op=>'<', value=>$self->_date } },
450 'order_by' => 'ORDER BY _date DESC LIMIT 1',
453 $self->get('previous_bill');
458 Returns the customer's invoice that follows this one
464 if (!$self->get('following_bill')) {
465 $self->set('following_bill', qsearchs({
466 table => 'cust_bill',
468 custnum => $self->custnum,
469 invnum => { op => '>', value => $self->invnum },
471 order_by => 'ORDER BY invnum ASC LIMIT 1',
474 $self->get('following_bill');
479 Returns a list consisting of the total previous balance for this customer,
480 followed by the previous outstanding invoices (as FS::cust_bill objects also).
486 # simple memoize; we use this a lot
487 if (!$self->get('previous')) {
489 my @cust_bill = sort { $a->_date <=> $b->_date }
490 grep { $_->owed != 0 }
491 qsearch( 'cust_bill', { 'custnum' => $self->custnum,
492 #'_date' => { op=>'<', value=>$self->_date },
493 'invnum' => { op=>'<', value=>$self->invnum },
496 foreach ( @cust_bill ) { $total += $_->owed; }
497 $self->set('previous', [$total, @cust_bill]);
499 return @{ $self->get('previous') };
502 =item enable_previous
504 Whether to show the 'Previous Charges' section when printing this invoice.
505 The negation of the 'disable_previous_balance' config setting.
509 sub enable_previous {
511 my $agentnum = $self->cust_main->agentnum;
512 !$self->conf->exists('disable_previous_balance', $agentnum);
517 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
524 { 'table' => 'cust_bill_pkg',
525 'hashref' => { 'invnum' => $self->invnum },
526 'order_by' => 'ORDER BY billpkgnum', #important? otherwise we could use
527 # the AUTLOADED FK search. or should
528 # that default to ORDER by the pkey?
533 =item cust_bill_pkg_pkgnum PKGNUM
535 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
540 sub cust_bill_pkg_pkgnum {
541 my( $self, $pkgnum ) = @_;
543 { 'table' => 'cust_bill_pkg',
544 'hashref' => { 'invnum' => $self->invnum,
547 'order_by' => 'ORDER BY billpkgnum',
554 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
561 my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
562 $self->cust_bill_pkg;
564 grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
569 Returns true if any of the packages (or their definitions) corresponding to the
570 line items for this invoice have the no_auto flag set.
576 grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
579 =item open_cust_bill_pkg
581 Returns the open line items for this invoice.
583 Note that cust_bill_pkg with both setup and recur fees are returned as two
584 separate line items, each with only one fee.
588 # modeled after cust_main::open_cust_bill
589 sub open_cust_bill_pkg {
592 # grep { $_->owed > 0 } $self->cust_bill_pkg
594 my %other = ( 'recur' => 'setup',
595 'setup' => 'recur', );
597 foreach my $field ( qw( recur setup )) {
598 push @open, map { $_->set( $other{$field}, 0 ); $_; }
599 grep { $_->owed($field) > 0 }
600 $self->cust_bill_pkg;
608 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
612 #false laziness w/cust_pkg.pm
616 'table' => 'cust_event',
617 'addl_from' => 'JOIN part_event USING ( eventpart )',
618 'hashref' => { 'tablenum' => $self->invnum },
619 'extra_sql' => " AND eventtable = 'cust_bill' ",
625 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
629 #false laziness w/cust_pkg.pm
633 "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
634 " WHERE tablenum = ? AND eventtable = 'cust_bill'";
635 my $sth = dbh->prepare($sql) or die dbh->errstr. " preparing $sql";
636 $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
637 $sth->fetchrow_arrayref->[0];
642 Returns the customer (see L<FS::cust_main>) for this invoice.
646 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
648 Returns a list: an empty list on success or a list of errors.
655 grep { $_->suspend(@_) }
656 grep {! $_->getfield('cancel') }
661 =item cust_suspend_if_balance_over AMOUNT
663 Suspends the customer associated with this invoice if the total amount owed on
664 this invoice and all older invoices is greater than the specified amount.
666 Returns a list: an empty list on success or a list of errors.
670 sub cust_suspend_if_balance_over {
671 my( $self, $amount ) = ( shift, shift );
672 my $cust_main = $self->cust_main;
673 if ( $cust_main->total_owed_date($self->_date) < $amount ) {
676 $cust_main->suspend(@_);
682 Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
687 my( $self, %opt ) = @_;
689 warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
690 join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
693 return ( 'Access denied' )
694 unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
696 my @pkgs = $self->cust_pkg;
698 if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
700 my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
701 warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
706 map { $_->cancel(%opt) }
707 grep { ! $_->getfield('cancel') }
713 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
719 map { $_ } #return $self->num_cust_bill_pay unless wantarray;
720 sort { $a->_date <=> $b->_date }
721 qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
726 =item cust_credit_bill
728 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
734 map { $_ } #return $self->num_cust_credit_bill unless wantarray;
735 sort { $a->_date <=> $b->_date }
736 qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
740 sub cust_credit_bill {
741 shift->cust_credited(@_);
744 #=item cust_bill_pay_pkgnum PKGNUM
746 #Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
747 #with matching pkgnum.
751 #sub cust_bill_pay_pkgnum {
752 # my( $self, $pkgnum ) = @_;
753 # map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
754 # sort { $a->_date <=> $b->_date }
755 # qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
756 # 'pkgnum' => $pkgnum,
761 =item cust_bill_pay_pkg PKGNUM
763 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
764 applied against the matching pkgnum.
768 sub cust_bill_pay_pkg {
769 my( $self, $pkgnum ) = @_;
772 'select' => 'cust_bill_pay_pkg.*',
773 'table' => 'cust_bill_pay_pkg',
774 'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
775 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
776 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
777 " AND cust_bill_pkg.pkgnum = $pkgnum",
782 #=item cust_credited_pkgnum PKGNUM
784 #=item cust_credit_bill_pkgnum PKGNUM
786 #Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
787 #with matching pkgnum.
791 #sub cust_credited_pkgnum {
792 # my( $self, $pkgnum ) = @_;
793 # map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
794 # sort { $a->_date <=> $b->_date }
795 # qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
796 # 'pkgnum' => $pkgnum,
801 #sub cust_credit_bill_pkgnum {
802 # shift->cust_credited_pkgnum(@_);
805 =item cust_credit_bill_pkg PKGNUM
807 Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
808 applied against the matching pkgnum.
812 sub cust_credit_bill_pkg {
813 my( $self, $pkgnum ) = @_;
816 'select' => 'cust_credit_bill_pkg.*',
817 'table' => 'cust_credit_bill_pkg',
818 'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
819 ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
820 'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
821 " AND cust_bill_pkg.pkgnum = $pkgnum",
826 =item cust_bill_batch
828 Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
832 sub cust_bill_batch {
834 qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
839 Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
840 hash keyed by term length.
846 FS::discount_plan->all($self);
851 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
858 my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
860 foreach (@taxlines) { $total += $_->setup; }
866 Returns the amount owed (still outstanding) on this invoice, which is charged
867 minus all payment applications (see L<FS::cust_bill_pay>) and credit
868 applications (see L<FS::cust_credit_bill>).
874 my $balance = $self->charged;
875 $balance -= $_->amount foreach ( $self->cust_bill_pay );
876 $balance -= $_->amount foreach ( $self->cust_credited );
877 $balance = sprintf( "%.2f", $balance);
878 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
882 =item owed_on_invoice
884 Returns the amount to be displayed as the "Balance Due" on this
885 invoice. Amount returned depends on conf flags for invoicing
887 See L<FS::cust_bill::owed> for the true amount currently owed
891 sub owed_on_invoice {
894 #return $self->owed()
895 # unless $self->conf->exists('previous_balance-payments_since')
897 # Add charges from this invoice
898 my $owed = $self->charged();
900 # Add carried balances from previous invoices
901 # If previous items aren't to be displayed on the invoice,
902 # _items_previous() is aware of this and responds appropriately.
903 $owed += $_->{amount} for $self->_items_previous();
905 # Subtract payments and credits displayed on this invoice
906 $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
912 my( $self, $pkgnum ) = @_;
914 #my $balance = $self->charged;
916 $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
918 $balance -= $_->amount for $self->cust_bill_pay_pkg($pkgnum);
919 $balance -= $_->amount for $self->cust_credit_bill_pkg($pkgnum);
921 $balance = sprintf( "%.2f", $balance);
922 $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
928 Returns true if this invoice should be hidden. See the
929 selfservice-hide_invoices-taxclass configuraiton setting.
935 my $conf = $self->conf;
936 my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
938 my @cust_bill_pkg = $self->cust_bill_pkg;
939 my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
940 ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
943 =item apply_payments_and_credits [ OPTION => VALUE ... ]
945 Applies unapplied payments and credits to this invoice.
946 Payments with the no_auto_apply flag set will not be applied.
948 A hash of optional arguments may be passed. Currently "manual" is supported.
949 If true, a payment receipt is sent instead of a statement when
950 'payment_receipt_email' configuration option is set.
952 If there is an error, returns the error, otherwise returns false.
956 sub apply_payments_and_credits {
957 my( $self, %options ) = @_;
958 my $conf = $self->conf;
960 local $SIG{HUP} = 'IGNORE';
961 local $SIG{INT} = 'IGNORE';
962 local $SIG{QUIT} = 'IGNORE';
963 local $SIG{TERM} = 'IGNORE';
964 local $SIG{TSTP} = 'IGNORE';
965 local $SIG{PIPE} = 'IGNORE';
967 my $oldAutoCommit = $FS::UID::AutoCommit;
968 local $FS::UID::AutoCommit = 0;
971 $self->select_for_update; #mutex
973 my @payments = grep { $_->unapplied > 0 }
974 grep { !$_->no_auto_apply }
975 $self->cust_main->cust_pay;
976 my @credits = grep { $_->credited > 0 } $self->cust_main->cust_credit;
978 if ( $conf->exists('pkg-balances') ) {
979 # limit @payments & @credits to those w/ a pkgnum grepped from $self
980 my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
981 @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
982 @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
985 while ( $self->owed > 0 and ( @payments || @credits ) ) {
988 if ( @payments && @credits ) {
990 #decide which goes first by weight of top (unapplied) line item
992 my @open_lineitems = $self->open_cust_bill_pkg;
995 max( map { $_->part_pkg->pay_weight || 0 }
1000 my $max_credit_weight =
1001 max( map { $_->part_pkg->credit_weight || 0 }
1003 map { $_->cust_pkg }
1007 #if both are the same... payments first? it has to be something
1008 if ( $max_pay_weight >= $max_credit_weight ) {
1014 } elsif ( @payments ) {
1016 } elsif ( @credits ) {
1019 die "guru meditation #12 and 35";
1023 if ( $app eq 'pay' ) {
1025 my $payment = shift @payments;
1026 $unapp_amount = $payment->unapplied;
1027 $app = new FS::cust_bill_pay { 'paynum' => $payment->paynum };
1028 $app->pkgnum( $payment->pkgnum )
1029 if $conf->exists('pkg-balances') && $payment->pkgnum;
1031 } elsif ( $app eq 'credit' ) {
1033 my $credit = shift @credits;
1034 $unapp_amount = $credit->credited;
1035 $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
1036 $app->pkgnum( $credit->pkgnum )
1037 if $conf->exists('pkg-balances') && $credit->pkgnum;
1040 die "guru meditation #12 and 35";
1044 if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
1045 warn "owed_pkgnum ". $app->pkgnum;
1046 $owed = $self->owed_pkgnum($app->pkgnum);
1048 $owed = $self->owed;
1050 next unless $owed > 0;
1052 warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
1053 $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
1055 $app->invnum( $self->invnum );
1057 my $error = $app->insert(%options);
1059 $dbh->rollback if $oldAutoCommit;
1060 return "Error inserting ". $app->table. " record: $error";
1062 die $error if $error;
1066 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1073 Sends this invoice to the destinations configured for this customer: sends
1074 email, prints and/or faxes. See L<FS::cust_main_invoice>.
1076 Options can be passed as a hashref. Positional parameters are no longer
1079 I<template>: a suffix for alternate invoices
1081 I<agentnum>: obsolete, now does nothing.
1083 I<from> overrides the default email invoice From: address.
1085 I<amount>: obsolete, does nothing
1087 I<notice_name> overrides "Invoice" as the name of the sent document
1088 (templates from 10/2009 or newer required).
1090 I<lpr> overrides the system 'lpr' option as the command to print a document
1091 from standard input.
1097 my $opt = ref($_[0]) ? $_[0] : +{ @_ };
1098 my $conf = $self->conf;
1100 my $cust_main = $self->cust_main;
1102 my @invoicing_list = $cust_main->invoicing_list;
1105 if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
1106 && ! $cust_main->invoice_noemail;
1109 if grep { $_ eq 'POST' } @invoicing_list; #postal
1111 #this has never been used post-$ORIGINAL_ISP afaik
1112 $self->fax_invoice($opt)
1113 if grep { $_ eq 'FAX' } @invoicing_list; #fax
1121 my $opt = shift || {};
1122 if ($opt and !ref($opt)) {
1123 die ref($self). '->email called with positional parameters';
1126 my $conf = $self->conf;
1128 my $from = delete $opt->{from};
1130 # this is where we set the From: address
1131 $from ||= $self->_agent_invoice_from || #XXX should go away
1132 $conf->invoice_from_full( $self->cust_main->agentnum );
1134 my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1136 if ( ! @invoicing_list ) { #no recipients
1137 if ( $conf->exists('cust_bill-no_recipients-error') ) {
1138 die 'No recipients for customer #'. $self->custnum;
1140 #default: better to notify this person than silence
1141 @invoicing_list = ($from);
1145 $self->SUPER::email( {
1147 'to' => \@invoicing_list,
1153 #this stays here for now because its explicitly used as
1154 # FS::cust_bill::queueable_email
1155 sub queueable_email {
1158 my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1159 or die "invalid invoice number: " . $opt{invnum};
1161 $self->set('mode', $opt{mode})
1164 my %args = map {$_ => $opt{$_}}
1166 qw( from notice_name no_coupon template );
1168 my $error = $self->email( \%args );
1169 die $error if $error;
1175 my $conf = $self->conf;
1177 #my $template = scalar(@_) ? shift : '';
1180 my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1183 my $cust_main = $self->cust_main;
1184 my $name = $cust_main->name;
1185 my $name_short = $cust_main->name_short;
1186 my $invoice_number = $self->invnum;
1187 my $invoice_date = $self->_date_pretty;
1189 eval qq("$subject");
1194 'Invoice-'. $self->invnum. '.pdf';
1197 =item lpr_data HASHREF
1199 Returns the postscript or plaintext for this invoice as an arrayref.
1201 Options must be passed as a hashref. Positional parameters are no longer
1204 I<template>, if specified, is the name of a suffix for alternate invoices.
1206 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1212 my $conf = $self->conf;
1213 my $opt = shift || {};
1214 if ($opt and !ref($opt)) {
1215 # nobody does this anyway
1216 die "FS::cust_bill::lpr_data called with positional parameters";
1219 my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1220 [ $self->$method( $opt ) ];
1225 Prints this invoice.
1227 Options must be passed as a hashref.
1229 I<template>, if specified, is the name of a suffix for alternate invoices.
1231 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1237 return if $self->hide;
1238 my $conf = $self->conf;
1239 my $opt = shift || {};
1240 if ($opt and !ref($opt)) {
1241 die "FS::cust_bill::print called with positional parameters";
1244 my $lpr = delete $opt->{lpr};
1245 if($conf->exists('invoice_print_pdf')) {
1246 # Add the invoice to the current batch.
1247 $self->batch_invoice($opt);
1251 $self->lpr_data($opt),
1252 'agentnum' => $self->cust_main->agentnum,
1258 =item fax_invoice HASHREF
1262 Options must be passed as a hashref.
1264 I<template>, if specified, is the name of a suffix for alternate invoices.
1266 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1272 return if $self->hide;
1273 my $conf = $self->conf;
1274 my $opt = shift || {};
1275 if ($opt and !ref($opt)) {
1276 die "FS::cust_bill::fax_invoice called with positional parameters";
1279 die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1280 unless $conf->exists('invoice_latex');
1282 my $dialstring = $self->cust_main->getfield('fax');
1285 my $error = send_fax( 'docdata' => $self->lpr_data($opt),
1286 'dialstring' => $dialstring,
1288 die $error if $error;
1292 =item batch_invoice [ HASHREF ]
1294 Place this invoice into the open batch (see C<FS::bill_batch>). If there
1295 isn't an open batch, one will be created.
1297 HASHREF may contain any options to be passed to C<print_pdf>.
1302 my ($self, $opt) = @_;
1303 my $bill_batch = $self->get_open_bill_batch;
1304 my $cust_bill_batch = FS::cust_bill_batch->new({
1305 batchnum => $bill_batch->batchnum,
1306 invnum => $self->invnum,
1308 if ( $self->mode ) {
1309 $opt->{mode} ||= $self->mode;
1310 $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
1312 return $cust_bill_batch->insert($opt);
1315 =item get_open_batch
1317 Returns the currently open batch as an FS::bill_batch object, creating a new
1318 one if necessary. (A per-agent batch if invoice_print_pdf-spoolagent is
1323 sub get_open_bill_batch {
1325 my $conf = $self->conf;
1326 my $hashref = { status => 'O' };
1327 $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1328 ? $self->cust_main->agentnum
1330 my $batch = qsearchs('bill_batch', $hashref);
1331 return $batch if $batch;
1332 $batch = FS::bill_batch->new($hashref);
1333 my $error = $batch->insert;
1334 die $error if $error;
1338 =item ftp_invoice [ TEMPLATENAME ]
1340 Sends this invoice data via FTP.
1342 TEMPLATENAME is unused?
1348 my $conf = $self->conf;
1349 my $template = scalar(@_) ? shift : '';
1352 'protocol' => 'ftp',
1353 'server' => $conf->config('cust_bill-ftpserver'),
1354 'username' => $conf->config('cust_bill-ftpusername'),
1355 'password' => $conf->config('cust_bill-ftppassword'),
1356 'dir' => $conf->config('cust_bill-ftpdir'),
1357 'format' => $conf->config('cust_bill-ftpformat'),
1361 =item spool_invoice [ TEMPLATENAME ]
1363 Spools this invoice data (see L<FS::spool_csv>)
1365 TEMPLATENAME is unused?
1371 my $conf = $self->conf;
1372 my $template = scalar(@_) ? shift : '';
1375 'format' => $conf->config('cust_bill-spoolformat'),
1376 'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1380 =item send_csv OPTION => VALUE, ...
1382 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1386 protocol - currently only "ftp"
1392 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1393 and YYMMDDHHMMSS is a timestamp.
1395 See L</print_csv> for a description of the output format.
1400 my($self, %opt) = @_;
1404 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1405 mkdir $spooldir, 0700 unless -d $spooldir;
1407 # don't localize dates here, they're a defined format
1408 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1409 my $file = "$spooldir/$tracctnum.csv";
1411 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1413 open(CSV, ">$file") or die "can't open $file: $!";
1421 if ( $opt{protocol} eq 'ftp' ) {
1422 eval "use Net::FTP;";
1424 $net = Net::FTP->new($opt{server}) or die @$;
1426 die "unknown protocol: $opt{protocol}";
1429 $net->login( $opt{username}, $opt{password} )
1430 or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1432 $net->binary or die "can't set binary mode";
1434 $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1436 $net->put($file) or die "can't put $file: $!";
1446 Spools CSV invoice data.
1452 =item format - any of FS::Misc::::Invoicing::spool_formats
1454 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1455 customer has the corresponding invoice destinations set (see
1456 L<FS::cust_main_invoice>).
1458 =item agent_spools - if set to a true value, will spool to per-agent files
1459 rather than a single global file
1461 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1462 append to that spool. L<FS::Cron::upload> will then send the spool file to
1465 =item balanceover - if set, only spools the invoice if the total amount owed on
1466 this invoice and all older invoices is greater than the specified amount.
1468 =item time - the "current time". Controls the printing of past due messages
1476 my($self, %opt) = @_;
1478 my $time = $opt{'time'} || time;
1479 my $cust_main = $self->cust_main;
1481 if ( $opt{'dest'} ) {
1482 my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1483 $cust_main->invoicing_list;
1484 return 'N/A' unless $invoicing_list{$opt{'dest'}}
1485 || ! keys %invoicing_list;
1488 if ( $opt{'balanceover'} ) {
1490 if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1493 my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1494 mkdir $spooldir, 0700 unless -d $spooldir;
1496 my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1499 if ( $opt{'agent_spools'} ) {
1500 $file = 'agentnum'.$cust_main->agentnum;
1505 if ( $opt{'upload_targetnum'} ) {
1506 $spooldir .= '/target'.$opt{'upload_targetnum'};
1507 mkdir $spooldir, 0700 unless -d $spooldir;
1508 } # otherwise it just goes into export.xxx/cust_bill
1510 if ( lc($opt{'format'}) eq 'billco' ) {
1514 $file = "$spooldir/$file.csv";
1516 my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1518 open(CSV, ">>$file") or die "can't open $file: $!";
1519 flock(CSV, LOCK_EX);
1524 if ( lc($opt{'format'}) eq 'billco' ) {
1526 flock(CSV, LOCK_UN);
1529 $file =~ s/-header.csv$/-detail.csv/;
1531 open(CSV,">>$file") or die "can't open $file: $!";
1532 flock(CSV, LOCK_EX);
1536 print CSV $detail if defined($detail);
1538 flock(CSV, LOCK_UN);
1545 =item print_csv OPTION => VALUE, ...
1547 Returns CSV data for this invoice.
1551 format - 'default', 'billco', 'oneline', 'bridgestone'
1553 Returns a list consisting of two scalars. The first is a single line of CSV
1554 header information for this invoice. The second is one or more lines of CSV
1555 detail information for this invoice.
1557 If I<format> is not specified or "default", the fields of the CSV file are as
1560 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1561 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1565 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1567 B<record_type> is C<cust_bill> for the initial header line only. The
1568 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1569 fields are filled in.
1571 B<record_type> is C<cust_bill_pkg> for detail lines. Only the first two fields
1572 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1575 =item invnum - invoice number
1577 =item custnum - customer number
1579 =item _date - invoice date
1581 =item charged - total invoice amount
1583 =item first - customer first name
1585 =item last - customer first name
1587 =item company - company name
1589 =item address1 - address line 1
1591 =item address2 - address line 1
1601 =item pkg - line item description
1603 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1605 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1607 =item sdate - start date for recurring fee
1609 =item edate - end date for recurring fee
1613 If I<format> is "billco", the fields of the header CSV file are as follows:
1615 +-------------------------------------------------------------------+
1616 | FORMAT HEADER FILE |
1617 |-------------------------------------------------------------------|
1618 | Field | Description | Name | Type | Width |
1619 | 1 | N/A-Leave Empty | RC | CHAR | 2 |
1620 | 2 | N/A-Leave Empty | CUSTID | CHAR | 15 |
1621 | 3 | Transaction Account No | TRACCTNUM | CHAR | 15 |
1622 | 4 | Transaction Invoice No | TRINVOICE | CHAR | 15 |
1623 | 5 | Transaction Zip Code | TRZIP | CHAR | 5 |
1624 | 6 | Transaction Company Bill To | TRCOMPANY | CHAR | 30 |
1625 | 7 | Transaction Contact Bill To | TRNAME | CHAR | 30 |
1626 | 8 | Additional Address Unit Info | TRADDR1 | CHAR | 30 |
1627 | 9 | Bill To Street Address | TRADDR2 | CHAR | 30 |
1628 | 10 | Ancillary Billing Information | TRADDR3 | CHAR | 30 |
1629 | 11 | Transaction City Bill To | TRCITY | CHAR | 20 |
1630 | 12 | Transaction State Bill To | TRSTATE | CHAR | 2 |
1631 | 13 | Bill Cycle Close Date | CLOSEDATE | CHAR | 10 |
1632 | 14 | Bill Due Date | DUEDATE | CHAR | 10 |
1633 | 15 | Previous Balance | BALFWD | NUM* | 9 |
1634 | 16 | Pmt/CR Applied | CREDAPPLY | NUM* | 9 |
1635 | 17 | Total Current Charges | CURRENTCHG | NUM* | 9 |
1636 | 18 | Total Amt Due | TOTALDUE | NUM* | 9 |
1637 | 19 | Total Amt Due | AMTDUE | NUM* | 9 |
1638 | 20 | 30 Day Aging | AMT30 | NUM* | 9 |
1639 | 21 | 60 Day Aging | AMT60 | NUM* | 9 |
1640 | 22 | 90 Day Aging | AMT90 | NUM* | 9 |
1641 | 23 | Y/N | AGESWITCH | CHAR | 1 |
1642 | 24 | Remittance automation | SCANLINE | CHAR | 100 |
1643 | 25 | Total Taxes & Fees | TAXTOT | NUM* | 9 |
1644 | 26 | Customer Reference Number | CUSTREF | CHAR | 15 |
1645 | 27 | Federal Tax*** | FEDTAX | NUM* | 9 |
1646 | 28 | State Tax*** | STATETAX | NUM* | 9 |
1647 | 29 | Other Taxes & Fees*** | OTHERTAX | NUM* | 9 |
1648 +-------+-------------------------------+------------+------+-------+
1650 If I<format> is "billco", the fields of the detail CSV file are as follows:
1652 FORMAT FOR DETAIL FILE
1654 Field | Description | Name | Type | Width
1655 1 | N/A-Leave Empty | RC | CHAR | 2
1656 2 | N/A-Leave Empty | CUSTID | CHAR | 15
1657 3 | Account Number | TRACCTNUM | CHAR | 15
1658 4 | Invoice Number | TRINVOICE | CHAR | 15
1659 5 | Line Sequence (sort order) | LINESEQ | NUM | 6
1660 6 | Transaction Detail | DETAILS | CHAR | 100
1661 7 | Amount | AMT | NUM* | 9
1662 8 | Line Format Control** | LNCTRL | CHAR | 2
1663 9 | Grouping Code | GROUP | CHAR | 2
1664 10 | User Defined | ACCT CODE | CHAR | 15
1666 If format is 'oneline', there is no detail file. Each invoice has a
1667 header line only, with the fields:
1669 Agent number, agent name, customer number, first name, last name, address
1670 line 1, address line 2, city, state, zip, invoice date, invoice number,
1671 amount charged, amount due, previous balance, due date.
1673 and then, for each line item, three columns containing the package number,
1674 description, and amount.
1676 If format is 'bridgestone', there is no detail file. Each invoice has a
1677 header line with the following fields in a fixed-width format:
1679 Customer number (in display format), date, name (first last), company,
1680 address 1, address 2, city, state, zip.
1682 This is a mailing list format, and has no per-invoice fields. To avoid
1683 sending redundant notices, the spooling event should have a "once" or
1684 "once_percust_every" condition.
1689 my($self, %opt) = @_;
1691 eval "use Text::CSV_XS";
1694 my $cust_main = $self->cust_main;
1696 my $csv = Text::CSV_XS->new({'always_quote'=>1});
1697 my $format = lc($opt{'format'});
1699 my $time = $opt{'time'} || time;
1701 $self->set('_template', $opt{template})
1702 if exists $opt{template};
1704 my $tracctnum = ''; #leaking out from billco-specific sections :/
1705 if ( $format eq 'billco' ) {
1708 $self->conf->config('billco-account_num', $cust_main->agentnum);
1710 $tracctnum = $account_num eq 'display_custnum'
1711 ? $cust_main->display_custnum
1712 : $opt{'tracctnum'};
1715 $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1717 my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1719 my( $previous_balance, @unused ) = $self->previous; #previous balance
1721 my $pmt_cr_applied = 0;
1722 $pmt_cr_applied += $_->{'amount'}
1723 foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1725 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1728 '', # 1 | N/A-Leave Empty CHAR 2
1729 '', # 2 | N/A-Leave Empty CHAR 15
1730 $tracctnum, # 3 | Transaction Account No CHAR 15
1731 $self->invnum, # 4 | Transaction Invoice No CHAR 15
1732 $cust_main->zip, # 5 | Transaction Zip Code CHAR 5
1733 $cust_main->company, # 6 | Transaction Company Bill To CHAR 30
1734 #$cust_main->payname, # 7 | Transaction Contact Bill To CHAR 30
1735 $cust_main->contact, # 7 | Transaction Contact Bill To CHAR 30
1736 $cust_main->address2, # 8 | Additional Address Unit Info CHAR 30
1737 $cust_main->address1, # 9 | Bill To Street Address CHAR 30
1738 '', # 10 | Ancillary Billing Information CHAR 30
1739 $cust_main->city, # 11 | Transaction City Bill To CHAR 20
1740 $cust_main->state, # 12 | Transaction State Bill To CHAR 2
1743 time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR 10
1746 $duedate, # 14 | Bill Due Date CHAR 10
1748 $previous_balance, # 15 | Previous Balance NUM* 9
1749 $pmt_cr_applied, # 16 | Pmt/CR Applied NUM* 9
1750 sprintf("%.2f", $self->charged), # 17 | Total Current Charges NUM* 9
1751 $totaldue, # 18 | Total Amt Due NUM* 9
1752 $totaldue, # 19 | Total Amt Due NUM* 9
1753 '', # 20 | 30 Day Aging NUM* 9
1754 '', # 21 | 60 Day Aging NUM* 9
1755 '', # 22 | 90 Day Aging NUM* 9
1756 'N', # 23 | Y/N CHAR 1
1757 '', # 24 | Remittance automation CHAR 100
1758 $taxtotal, # 25 | Total Taxes & Fees NUM* 9
1759 $self->custnum, # 26 | Customer Reference Number CHAR 15
1760 '0', # 27 | Federal Tax*** NUM* 9
1761 sprintf("%.2f", $taxtotal), # 28 | State Tax*** NUM* 9
1762 '0', # 29 | Other Taxes & Fees*** NUM* 9
1765 } elsif ( $format eq 'oneline' ) { #name
1767 my ($previous_balance) = $self->previous;
1768 $previous_balance = sprintf('%.2f', $previous_balance);
1769 my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1775 $self->_items_pkg, #_items_nontax? no sections or anything
1780 $cust_main->agentnum,
1781 $cust_main->agent->agent,
1785 $cust_main->company,
1786 $cust_main->address1,
1787 $cust_main->address2,
1793 time2str("%x", $self->_date),
1798 $self->due_date2str("%x"),
1803 } elsif ( $format eq 'bridgestone' ) {
1805 # bypass the CSV stuff and just return this
1806 my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1807 my $zip = $cust_main->zip;
1809 my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1813 "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1815 $cust_main->display_custnum,
1817 uc(substr($cust_main->contact_firstlast,0,30)),
1818 uc(substr($cust_main->company ,0,30)),
1819 uc(substr($cust_main->address1 ,0,30)),
1820 uc(substr($cust_main->address2 ,0,30)),
1821 uc(substr($cust_main->city ,0,20)),
1822 uc($cust_main->state),
1828 } elsif ( $format eq 'ics' ) {
1830 my $bill = $cust_main->bill_location;
1831 my $zip = $bill->zip;
1835 if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1840 # minor false laziness with print_generic
1841 my ($previous_balance) = $self->previous;
1842 my $balance_due = $self->owed + $previous_balance;
1843 my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1844 my $credit_total = sum(0, map { $_->{'amount'} } $self->_items_credits);
1847 if ( $self->due_date and $time >= $self->due_date ) {
1848 $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1852 my $header = sprintf(
1853 '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1854 $cust_main->display_custnum, #BID
1855 uc($cust_main->first), #FNAME
1856 uc($cust_main->last), #LNAME
1857 '00', #BATCH, should this ever be anything else?
1858 uc($cust_main->company), #COMP
1859 uc($bill->address1), #STREET1
1860 uc($bill->address2), #STREET2
1861 uc($bill->city), #CITY
1862 uc($bill->state), #STATE
1865 time2str('%Y%m%d', $self->_date), #BILL_DATE
1866 $self->due_date2str('%Y%m%d'), #DUE_DATE,
1867 ( map {sprintf('%0.2f', $_)}
1868 $balance_due, #AMNT_DUE
1869 $previous_balance, #PREV_BAL
1870 $payment_total, #PYMT_RCVD
1871 $credit_total, #CREDITS
1872 $previous_balance, #BEG_BAL--is this correct?
1873 $self->charged, #NEW_CHRG
1876 $past_due, #PAST_MSG
1880 my %svc_class = ('' => ''); # maybe cache this more persistently?
1882 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1884 my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1885 my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1889 my @dates = ( $self->_date, undef );
1890 if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1891 $dates[1] = $prev->sdate; #questionable
1894 # generate an 01 detail for each service
1895 my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1896 foreach my $cust_svc ( @svcs ) {
1897 $show_pkgnum = ''; # hide it if we're showing svcnums
1899 my $svcpart = $cust_svc->svcpart;
1900 if (!exists($svc_class{$svcpart})) {
1901 my $classnum = $cust_svc->part_svc->classnum;
1902 my $part_svc_class = FS::part_svc_class->by_key($classnum)
1904 $svc_class{$svcpart} = $part_svc_class ?
1905 $part_svc_class->classname :
1909 my @h_label = $cust_svc->label(@dates, 'I');
1910 push @details, sprintf('01%-9s%-20s%-47s',
1912 $svc_class{$svcpart},
1915 } #foreach $cust_svc
1918 my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1919 if ($cust_bill_pkg->recur > 0) {
1920 $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1921 time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1923 push @details, sprintf('02%-6s%-60s%-10s',
1926 sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1928 } #foreach $cust_bill_pkg
1930 # Tag this row so that we know whether this is one page (1), two pages
1931 # (2), # or "big" (B). The tag will be stripped off before uploading.
1932 if ( scalar(@details) < 12 ) {
1934 } elsif ( scalar(@details) < 58 ) {
1940 return join('', $header, @details, "\n");
1948 time2str("%x", $self->_date),
1949 sprintf("%.2f", $self->charged),
1950 ( map { $cust_main->getfield($_) }
1951 qw( first last company address1 address2 city state zip country ) ),
1953 ) or die "can't create csv";
1956 my $header = $csv->string. "\n";
1959 if ( lc($opt{'format'}) eq 'billco' ) {
1962 my %items_opt = ( format => 'template',
1963 escape_function => sub { shift } );
1964 # I don't know what characters billco actually tolerates in spool entries.
1965 # Text::CSV will take care of delimiters, though.
1967 my @items = ( $self->_items_pkg(%items_opt),
1968 $self->_items_fee(%items_opt) );
1969 foreach my $item (@items) {
1971 my $description = $item->{'description'};
1972 if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1973 $description .= ': ' . $item->{ext_description}[0];
1977 '', # 1 | N/A-Leave Empty CHAR 2
1978 '', # 2 | N/A-Leave Empty CHAR 15
1979 $tracctnum, # 3 | Account Number CHAR 15
1980 $self->invnum, # 4 | Invoice Number CHAR 15
1981 $lineseq++, # 5 | Line Sequence (sort order) NUM 6
1982 $description, # 6 | Transaction Detail CHAR 100
1983 $item->{'amount'}, # 7 | Amount NUM* 9
1984 '', # 8 | Line Format Control** CHAR 2
1985 '', # 9 | Grouping Code CHAR 2
1986 '', # 10 | User Defined CHAR 15
1989 $detail .= $csv->string. "\n";
1993 } elsif ( lc($opt{'format'}) eq 'oneline' ) {
1999 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2001 my($pkg, $setup, $recur, $sdate, $edate);
2002 if ( $cust_bill_pkg->pkgnum ) {
2004 ($pkg, $setup, $recur, $sdate, $edate) = (
2005 $cust_bill_pkg->part_pkg->pkg,
2006 ( $cust_bill_pkg->setup != 0
2007 ? sprintf("%.2f", $cust_bill_pkg->setup )
2009 ( $cust_bill_pkg->recur != 0
2010 ? sprintf("%.2f", $cust_bill_pkg->recur )
2012 ( $cust_bill_pkg->sdate
2013 ? time2str("%x", $cust_bill_pkg->sdate)
2015 ($cust_bill_pkg->edate
2016 ? time2str("%x", $cust_bill_pkg->edate)
2020 } else { #pkgnum tax
2021 next unless $cust_bill_pkg->setup != 0;
2022 $pkg = $cust_bill_pkg->desc;
2023 $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2024 ( $sdate, $edate ) = ( '', '' );
2030 ( map { '' } (1..11) ),
2031 ($pkg, $setup, $recur, $sdate, $edate)
2032 ) or die "can't create csv";
2034 $detail .= $csv->string. "\n";
2040 ( $header, $detail );
2045 croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
2050 Attempts to pay this invoice with a credit card payment via a
2051 Business::OnlinePayment realtime gateway. See
2052 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2053 for supported processors.
2059 $self->realtime_bop( 'CC', @_ );
2064 Attempts to pay this invoice with an electronic check (ACH) payment via a
2065 Business::OnlinePayment realtime gateway. See
2066 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2067 for supported processors.
2073 $self->realtime_bop( 'ECHECK', @_ );
2078 Attempts to pay this invoice with phone bill (LEC) payment via a
2079 Business::OnlinePayment realtime gateway. See
2080 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2081 for supported processors.
2087 $self->realtime_bop( 'LEC', @_ );
2091 my( $self, $method ) = (shift,shift);
2092 my $conf = $self->conf;
2095 my $cust_main = $self->cust_main;
2096 my $balance = $cust_main->balance;
2097 my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2098 $amount = sprintf("%.2f", $amount);
2099 return "not run (balance $balance)" unless $amount > 0;
2101 my $description = 'Internet Services';
2102 if ( $conf->exists('business-onlinepayment-description') ) {
2103 my $dtempl = $conf->config('business-onlinepayment-description');
2105 my $agent_obj = $cust_main->agent
2106 or die "can't retreive agent for $cust_main (agentnum ".
2107 $cust_main->agentnum. ")";
2108 my $agent = $agent_obj->agent;
2109 my $pkgs = join(', ',
2110 map { $_->part_pkg->pkg }
2111 grep { $_->pkgnum } $self->cust_bill_pkg
2113 $description = eval qq("$dtempl");
2116 $cust_main->realtime_bop($method, $amount,
2117 'description' => $description,
2118 'invnum' => $self->invnum,
2119 #this didn't do what we want, it just calls apply_payments_and_credits
2121 'apply_to_invoice' => 1,
2124 #this changes application behavior: auto payments
2125 #triggered against a specific invoice are now applied
2126 #to that invoice instead of oldest open.
2132 =item batch_card OPTION => VALUE...
2134 Adds a payment for this invoice to the pending credit card batch (see
2135 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2136 runs the payment using a realtime gateway.
2141 my ($self, %options) = @_;
2142 my $cust_main = $self->cust_main;
2144 $options{invnum} = $self->invnum;
2146 $cust_main->batch_card(%options);
2149 sub _agent_template {
2151 $self->cust_main->agent_template;
2154 sub _agent_invoice_from {
2156 $self->cust_main->agent_invoice_from;
2159 =item invoice_barcode DIR_OR_FALSE
2161 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2162 it is taken as the temp directory where the PNG file will be generated and the
2163 PNG file name is returned. Otherwise, the PNG image itself is returned.
2167 sub invoice_barcode {
2168 my ($self, $dir) = (shift,shift);
2170 my $gdbar = new GD::Barcode('Code39',$self->invnum);
2171 die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2172 my $gd = $gdbar->plot(Height => 30);
2175 my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2179 ) or die "can't open temp file: $!\n";
2180 print $bh $gd->png or die "cannot write barcode to file: $!\n";
2181 my $png_file = $bh->filename;
2188 =item invnum_date_pretty
2190 Returns a string with the invoice number and date, for example:
2191 "Invoice #54 (3/20/2008)".
2193 Intended for back-end context, with regard to translation and date formatting.
2197 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2198 # for backend use (and also does the wrong thing, localizing for end customer
2199 # instead of backoffice configured date format)
2200 sub invnum_date_pretty {
2202 #$self->mt('Invoice #').
2203 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2204 $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2207 #sub _items_extra_usage_sections {
2209 # my $escape = shift;
2211 # my %sections = ();
2213 # my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
2214 # foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2216 # next unless $cust_bill_pkg->pkgnum > 0;
2218 # foreach my $section ( keys %usage_class ) {
2220 # my $usage = $cust_bill_pkg->usage($section);
2222 # next unless $usage && $usage > 0;
2224 # $sections{$section} ||= 0;
2225 # $sections{$section} += $usage;
2231 # map { { 'description' => &{$escape}($_),
2232 # 'subtotal' => $sections{$_},
2233 # 'summarized' => '',
2234 # 'tax_section' => '',
2237 # sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2241 sub _items_extra_usage_sections {
2243 my $conf = $self->conf;
2251 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2253 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2254 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2255 next unless $cust_bill_pkg->pkgnum > 0;
2257 foreach my $classnum ( keys %usage_class ) {
2258 my $section = $usage_class{$classnum}->classname;
2259 $classnums{$section} = $classnum;
2261 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2262 my $amount = $detail->amount;
2263 next unless $amount && $amount > 0;
2265 $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2266 $sections{$section}{amount} += $amount; #subtotal
2267 $sections{$section}{calls}++;
2268 $sections{$section}{duration} += $detail->duration;
2270 my $desc = $detail->regionname;
2271 my $description = $desc;
2272 $description = substr($desc, 0, $maxlength). '...'
2273 if $format eq 'latex' && length($desc) > $maxlength;
2275 $lines{$section}{$desc} ||= {
2276 description => &{$escape}($description),
2277 #pkgpart => $part_pkg->pkgpart,
2278 pkgnum => $cust_bill_pkg->pkgnum,
2283 #unit_amount => $cust_bill_pkg->unitrecur,
2284 quantity => $cust_bill_pkg->quantity,
2285 product_code => 'N/A',
2286 ext_description => [],
2289 $lines{$section}{$desc}{amount} += $amount;
2290 $lines{$section}{$desc}{calls}++;
2291 $lines{$section}{$desc}{duration} += $detail->duration;
2297 my %sectionmap = ();
2298 foreach (keys %sections) {
2299 my $usage_class = $usage_class{$classnums{$_}};
2300 $sectionmap{$_} = { 'description' => &{$escape}($_),
2301 'amount' => $sections{$_}{amount}, #subtotal
2302 'calls' => $sections{$_}{calls},
2303 'duration' => $sections{$_}{duration},
2305 'tax_section' => '',
2306 'sort_weight' => $usage_class->weight,
2307 ( $usage_class->format
2308 ? ( map { $_ => $usage_class->$_($format) }
2309 qw( description_generator header_generator total_generator total_line_generator )
2316 my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2320 foreach my $section ( keys %lines ) {
2321 foreach my $line ( keys %{$lines{$section}} ) {
2322 my $l = $lines{$section}{$line};
2323 $l->{section} = $sectionmap{$section};
2324 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2325 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2330 return(\@sections, \@lines);
2336 my $end = $self->_date;
2338 # start at date of previous invoice + 1 second or 0 if no previous invoice
2339 my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2340 $start = 0 if !$start;
2343 my $cust_main = $self->cust_main;
2344 my @pkgs = $cust_main->all_pkgs;
2345 my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2348 foreach my $pkg ( @pkgs ) {
2349 my @h_cust_svc = $pkg->h_cust_svc($end);
2350 foreach my $h_cust_svc ( @h_cust_svc ) {
2351 next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2352 next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2354 my $inserted = $h_cust_svc->date_inserted;
2355 my $deleted = $h_cust_svc->date_deleted;
2356 my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2358 $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
2360 # DID either activated or ported in; cannot be both for same DID simultaneously
2361 if ($inserted >= $start && $inserted <= $end && $phone_inserted
2362 && (!$phone_inserted->lnp_status
2363 || $phone_inserted->lnp_status eq ''
2364 || $phone_inserted->lnp_status eq 'native')) {
2367 else { # this one not so clean, should probably move to (h_)svc_phone
2368 local($FS::Record::qsearch_qualify_columns) = 0;
2369 my $phone_portedin = qsearchs( 'h_svc_phone',
2370 { 'svcnum' => $h_cust_svc->svcnum,
2371 'lnp_status' => 'portedin' },
2372 FS::h_svc_phone->sql_h_searchs($end),
2374 $num_portedin++ if $phone_portedin;
2377 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2378 if($deleted >= $start && $deleted <= $end && $phone_deleted
2379 && (!$phone_deleted->lnp_status
2380 || $phone_deleted->lnp_status ne 'portingout')) {
2383 elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2384 && $phone_deleted->lnp_status
2385 && $phone_deleted->lnp_status eq 'portingout') {
2389 # increment usage minutes
2390 if ( $phone_inserted ) {
2391 my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2392 $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2395 warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2398 # don't look at this service again
2399 push @seen, $h_cust_svc->svcnum;
2403 $minutes = sprintf("%d", $minutes);
2404 ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
2405 . "$num_deactivated Ported-Out: $num_portedout ",
2406 "Total Minutes: $minutes");
2409 sub _items_accountcode_cdr {
2414 my $section = { 'amount' => 0,
2417 'sort_weight' => '',
2419 'description' => 'Usage by Account Code',
2425 my %accountcodes = ();
2427 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2428 next unless $cust_bill_pkg->pkgnum > 0;
2430 my @header = $cust_bill_pkg->details_header;
2431 next unless scalar(@header);
2432 $section->{'header'} = join(',',@header);
2434 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2436 $section->{'header'} = $detail->formatted('format' => $format)
2437 if($detail->detail eq $section->{'header'});
2439 my $accountcode = $detail->accountcode;
2440 next unless $accountcode;
2442 my $amount = $detail->amount;
2443 next unless $amount && $amount > 0;
2445 $accountcodes{$accountcode} ||= {
2446 description => $accountcode,
2453 product_code => 'N/A',
2454 section => $section,
2455 ext_description => [ $section->{'header'} ],
2459 $section->{'amount'} += $amount;
2460 $accountcodes{$accountcode}{'amount'} += $amount;
2461 $accountcodes{$accountcode}{calls}++;
2462 $accountcodes{$accountcode}{duration} += $detail->duration;
2463 push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2467 foreach my $l ( values %accountcodes ) {
2468 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2469 my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2470 foreach my $sorted_detail ( @sorted_detail ) {
2471 push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2473 delete $l->{detail_temp};
2477 my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2479 return ($section,\@sorted_lines);
2482 sub _items_svc_phone_sections {
2484 my $conf = $self->conf;
2492 my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2494 my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2495 $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2497 foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2498 next unless $cust_bill_pkg->pkgnum > 0;
2500 my @header = $cust_bill_pkg->details_header;
2501 next unless scalar(@header);
2503 foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2505 my $phonenum = $detail->phonenum;
2506 next unless $phonenum;
2508 my $amount = $detail->amount;
2509 next unless $amount && $amount > 0;
2511 $sections{$phonenum} ||= { 'amount' => 0,
2514 'sort_weight' => -1,
2515 'phonenum' => $phonenum,
2517 $sections{$phonenum}{amount} += $amount; #subtotal
2518 $sections{$phonenum}{calls}++;
2519 $sections{$phonenum}{duration} += $detail->duration;
2521 my $desc = $detail->regionname;
2522 my $description = $desc;
2523 $description = substr($desc, 0, $maxlength). '...'
2524 if $format eq 'latex' && length($desc) > $maxlength;
2526 $lines{$phonenum}{$desc} ||= {
2527 description => &{$escape}($description),
2528 #pkgpart => $part_pkg->pkgpart,
2536 product_code => 'N/A',
2537 ext_description => [],
2540 $lines{$phonenum}{$desc}{amount} += $amount;
2541 $lines{$phonenum}{$desc}{calls}++;
2542 $lines{$phonenum}{$desc}{duration} += $detail->duration;
2544 my $line = $usage_class{$detail->classnum}->classname;
2545 $sections{"$phonenum $line"} ||=
2549 'sort_weight' => $usage_class{$detail->classnum}->weight,
2550 'phonenum' => $phonenum,
2551 'header' => [ @header ],
2553 $sections{"$phonenum $line"}{amount} += $amount; #subtotal
2554 $sections{"$phonenum $line"}{calls}++;
2555 $sections{"$phonenum $line"}{duration} += $detail->duration;
2557 $lines{"$phonenum $line"}{$desc} ||= {
2558 description => &{$escape}($description),
2559 #pkgpart => $part_pkg->pkgpart,
2567 product_code => 'N/A',
2568 ext_description => [],
2571 $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2572 $lines{"$phonenum $line"}{$desc}{calls}++;
2573 $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2574 push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2575 $detail->formatted('format' => $format);
2580 my %sectionmap = ();
2581 my $simple = new FS::usage_class { format => 'simple' }; #bleh
2582 foreach ( keys %sections ) {
2583 my @header = @{ $sections{$_}{header} || [] };
2585 new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2586 my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2587 my $usage_class = $summary ? $simple : $usage_simple;
2588 my $ending = $summary ? ' usage charges' : '';
2591 $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2593 $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2594 'amount' => $sections{$_}{amount}, #subtotal
2595 'calls' => $sections{$_}{calls},
2596 'duration' => $sections{$_}{duration},
2598 'tax_section' => '',
2599 'phonenum' => $sections{$_}{phonenum},
2600 'sort_weight' => $sections{$_}{sort_weight},
2601 'post_total' => $summary, #inspire pagebreak
2603 ( map { $_ => $usage_class->$_($format, %gen_opt) }
2604 qw( description_generator
2607 total_line_generator
2614 my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2615 $a->{sort_weight} <=> $b->{sort_weight}
2620 foreach my $section ( keys %lines ) {
2621 foreach my $line ( keys %{$lines{$section}} ) {
2622 my $l = $lines{$section}{$line};
2623 $l->{section} = $sectionmap{$section};
2624 $l->{amount} = sprintf( "%.2f", $l->{amount} );
2625 #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2630 if($conf->exists('phone_usage_class_summary')) {
2631 # this only works with Latex
2635 # after this, we'll have only two sections per DID:
2636 # Calls Summary and Calls Detail
2637 foreach my $section ( @sections ) {
2638 if($section->{'post_total'}) {
2639 $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2640 $section->{'total_line_generator'} = sub { '' };
2641 $section->{'total_generator'} = sub { '' };
2642 $section->{'header_generator'} = sub { '' };
2643 $section->{'description_generator'} = '';
2644 push @newsections, $section;
2645 my %calls_detail = %$section;
2646 $calls_detail{'post_total'} = '';
2647 $calls_detail{'sort_weight'} = '';
2648 $calls_detail{'description_generator'} = sub { '' };
2649 $calls_detail{'header_generator'} = sub {
2650 return ' & Date/Time & Called Number & Duration & Price'
2651 if $format eq 'latex';
2654 $calls_detail{'description'} = 'Calls Detail: '
2655 . $section->{'phonenum'};
2656 push @newsections, \%calls_detail;
2660 # after this, each usage class is collapsed/summarized into a single
2661 # line under the Calls Summary section
2662 foreach my $newsection ( @newsections ) {
2663 if($newsection->{'post_total'}) { # this means Calls Summary
2664 foreach my $section ( @sections ) {
2665 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
2666 && !$section->{'post_total'});
2667 my $newdesc = $section->{'description'};
2668 my $tn = $section->{'phonenum'};
2669 $newdesc =~ s/$tn//g;
2670 my $line = { ext_description => [],
2674 calls => $section->{'calls'},
2675 section => $newsection,
2676 duration => $section->{'duration'},
2677 description => $newdesc,
2678 amount => sprintf("%.2f",$section->{'amount'}),
2679 product_code => 'N/A',
2681 push @newlines, $line;
2686 # after this, Calls Details is populated with all CDRs
2687 foreach my $newsection ( @newsections ) {
2688 if(!$newsection->{'post_total'}) { # this means Calls Details
2689 foreach my $line ( @lines ) {
2690 next unless (scalar(@{$line->{'ext_description'}}) &&
2691 $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2693 my @extdesc = @{$line->{'ext_description'}};
2695 foreach my $extdesc ( @extdesc ) {
2696 $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2697 push @newextdesc, $extdesc;
2699 $line->{'ext_description'} = \@newextdesc;
2700 $line->{'section'} = $newsection;
2701 push @newlines, $line;
2706 return(\@newsections, \@newlines);
2709 return(\@sections, \@lines);
2713 =sub _items_usage_class_summary OPTIONS
2715 Returns a list of detail items summarizing the usage charges on this
2716 invoice. Each one will have 'amount', 'description' (the usage charge name),
2717 and 'usage_classnum'.
2719 OPTIONS can include 'escape' (a function to escape the descriptions).
2723 sub _items_usage_class_summary {
2727 my $escape = $opt{escape} || sub { $_[0] };
2728 my $money_char = $opt{money_char};
2729 my $invnum = $self->invnum;
2730 my @classes = qsearch({
2731 'table' => 'usage_class',
2732 'select' => 'classnum, classname, SUM(amount) AS amount,'.
2733 ' COUNT(*) AS calls, SUM(duration) AS duration',
2734 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2735 ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2736 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2737 ' GROUP BY classnum, classname, weight'.
2738 ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2739 ' ORDER BY weight ASC',
2743 description => &{$escape}($self->mt('Usage Summary')),
2747 foreach my $class (@classes) {
2748 $section->{subtotal} += $class->get('amount');
2750 'description' => &{$escape}($class->classname),
2751 'amount' => $money_char.sprintf('%.2f', $class->get('amount')),
2752 'quantity' => $class->get('calls'),
2753 'duration' => $class->get('duration'),
2754 'usage_classnum' => $class->classnum,
2755 'section' => $section,
2758 $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
2762 =sub _items_previous()
2764 Returns an array of hashrefs, each hashref representing a line-item on
2765 the current bill for previous unpaid invoices.
2767 keys for each previous_item:
2768 - amount (see notes)
2774 Payments and credits shown on this invoice may vary based on configuraiton.
2776 when conf flag previous_balance-payments_since is set:
2777 This method works backwards to rebuild the invoice as a snapshot in time.
2778 The invoice displayed will have the balances owed, and payments made,
2779 reflecting the state of the account at the time of invoice generation.
2783 sub _items_previous {
2788 if ($self->get('_items_previous')) {
2789 return sort { $a->{_date} <=> $b->{_date} }
2790 values %{ $self->get('_items_previous') };
2793 # Gets the customer's current balance and outstanding invoices.
2794 my ($prev_balance, @open_invoices) = $self->previous;
2796 my %invoices = map {
2797 $_->invnum => $self->__items_previous_map_invoice($_)
2800 # Which credits and payments displayed on the bill will vary based on
2801 # conf flag previous_balance-payments_since.
2802 my @credits = $self->_items_credits();
2803 my @payments = $self->_items_payments();
2806 if ($self->conf->exists('previous_balance-payments_since')) {
2807 # For each credit or payment, determine which invoices it was applied to.
2808 # Manipulate data displayed so the invoice displayed appears as a
2809 # snapshot in time... with previous balances and balance owed displayed
2810 # as they were at the time of invoice creation.
2812 my @credits_postbill = $self->_items_credits_postbill();
2813 my @payments_postbill = $self->_items_payments_postbill();
2818 # Each section below follows this pattern on a payment/credit
2820 # - Dupe check, avoid adjusting for the same item twice
2821 # - If invoice being adjusted for isn't in our list, add it
2822 # - Adjust the invoice balance to refelct balnace without the
2823 # credit or payment applied
2826 # Working with payments displayed on this bill
2827 for my $pmt_hash (@payments) {
2828 my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
2829 for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
2830 next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
2831 $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
2833 my $invnum = $cust_bill_pay->invnum;
2835 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2836 unless exists $invoices{$invnum};
2838 $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
2842 # Working with credits displayed on this bill
2843 for my $cred_hash (@credits) {
2844 my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
2845 for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
2846 next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
2847 $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
2849 my $invnum = $cust_credit_bill->invnum;
2851 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2852 unless exists $invoices{$invnum};
2854 $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
2858 # Working with both credits and payments which are not displayed
2859 # on this bill, but which have affected this bill's balances
2860 for my $postbill (@payments_postbill, @credits_postbill) {
2862 if ($postbill->{billpaynum}) {
2863 next if exists $pmnt_dupechk{$postbill->{billpaynum}};
2864 $pmnt_dupechk{$postbill->{billpaynum}} = 1;
2865 } elsif ($postbill->{creditbillnum}) {
2866 next if exists $cred_dupechk{$postbill->{creditbillnum}};
2867 $cred_dupechk{$postbill->{creditbillnum}} = 1;
2869 die "Missing creditbillnum or billpaynum";
2872 my $invnum = $postbill->{invnum};
2874 $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2875 unless exists $invoices{$invnum};
2877 $invoices{$invnum}->{amount} += $postbill->{amount};
2880 # Make sure current invoice doesn't appear in previous items
2881 delete $invoices{$self->invnum}
2882 if exists $invoices{$self->invnum};
2886 # Make sure amount is formatted as a dollar string
2887 # (Formatting should happen on the template side, but is not?)
2888 $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
2891 $self->set('_items_previous', \%invoices);
2892 return sort { $a->{_date} <=> $b->{_date} } values %invoices;
2896 =sub _items_previous_total
2898 Return sum of amounts from all items returned by _items_previous
2899 Results will vary based on invoicing conf flags
2903 sub _items_previous_total {
2906 $tot += $_->{amount} for $self->_items_previous();
2910 sub __items_previous_get_invoice {
2911 # Helper function for _items_previous
2913 # Read a record from cust_bill, return a hash of it's information
2914 my ($self, $invnum) = @_;
2915 die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
2917 my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
2918 return $self->__items_previous_map_invoice($cust_bill);
2921 sub __items_previous_map_invoice {
2922 # Helper function for _items_previous
2924 # Transform a cust_bill object into a simple hash reference of the type
2925 # required by _items_previous
2926 my ($self, $cust_bill) = @_;
2927 die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
2929 my $date = $self->conf->exists('invoice_show_prior_due_date')
2930 ? 'due '.$cust_bill->due_date2str('short')
2931 : $self->time2str_local('short', $cust_bill->_date);
2934 invnum => $cust_bill->invnum,
2935 amount => $cust_bill->owed,
2937 _date => $cust_bill->_date,
2938 description => join(' ',
2939 $self->mt('Previous Balance, Invoice #'),
2946 =sub _items_credits()
2948 Return array of hashrefs containing credits to be shown as line-items
2949 when rendering this bill.
2951 keys for each credit item:
2952 - crednum: id of payment
2953 - amount: payment amount
2954 - description: line item to be displayed on the bill
2956 This method has three ways it selects which credits to display on
2959 1) Default Case: No Conf flag for 'previous_balance-payments_since'
2961 Returns credits that have been applied to this bill only
2964 Conf flag set for 'previous_balance-payments_since'
2966 List all credits that have been recorded during the time period
2967 between the timestamps of the last invoice and this invoice
2970 Conf flag set for 'previous_balance-payments_since'
2971 $opt{'template'} eq 'statement'
2973 List all payments that have been recorded between the timestamps
2974 of the previous invoice and the following invoice.
2976 This is used to give the customer a receipt for a payment
2977 in the form of their last bill with the payment amended.
2979 I am concerned with this implementation, but leaving in place as is
2980 If this option is selected, while viewing an older bill, the old bill
2981 will show ALL future credits for future bills, but no charges for
2982 future bills. Somebody could be misled into believing they have a
2983 large account credit when they don't. Also, interrupts the chain of
2984 invoices as an account history... the customer could have two invoices
2985 in their fileing cabinet, for two different dates, both with a line item
2986 for the same duplicate credit. The accounting is technically accurate,
2987 but somebody could easily become confused and think two credits were
2988 made, when really those two line items on two different bills represent
2989 only a single credit
2993 sub _items_credits {
2998 return @{$self->get('_items_credits')} if $self->get('_items_credits');
3001 my $template = $opt{template} || $self->get('_template');
3002 my $trim_len = $opt{template} || $self->get('trim_len') || 40;
3005 my @cust_credit_objs;
3007 if ($self->conf->exists('previous_balance-payments_since')) {
3008 if ($template eq 'statement') {
3009 # Case 3 (see above)
3010 # Return credits timestamped between the previous and following bills
3012 my $previous_bill = $self->previous_bill;
3013 my $following_bill = $self->following_bill;
3015 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3016 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3019 table => 'cust_credit',
3021 custnum => $self->custnum,
3022 _date => { op => '>=', value => $date_start },
3025 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3027 @cust_credit_objs = qsearch(\%query);
3030 # Case 2 (see above)
3031 # Return credits timestamps between this and the previous bills
3034 my $date_end = $self->_date;
3036 my $previous_bill = $self->previous_bill;
3037 if (ref $previous_bill) {
3038 $date_start = $previous_bill->_date;
3041 @cust_credit_objs = qsearch({
3042 table => 'cust_credit',
3044 custnum => $self->custnum,
3045 _date => {op => '>=', value => $date_start},
3047 extra_sql => " AND _date <= $date_end ",
3052 # Case 1 (see above)
3053 # Return only credits that have been applied to this bill
3055 @cust_credit_objs = $self->cust_credited;
3059 # Translate objects into hashrefs
3060 foreach my $obj ( @cust_credit_objs ) {
3061 my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3063 amount => sprintf('%.2f',$cust_credit->amount),
3064 crednum => $cust_credit->crednum,
3065 _date => $cust_credit->_date,
3066 creditreason => $cust_credit->reason,
3069 my $reason = substr($cust_credit->reason, 0, $trim_len);
3070 $reason .= '...' if length($reason) < length($cust_credit->reason);
3071 $reason = "($reason)" if $reason;
3073 $r_obj{description} = join(' ',
3074 $self->mt('Credit applied'),
3075 $self->time2str_local('short', $cust_credit->_date),
3079 push @return, \%r_obj;
3081 $self->set('_items_credits',\@return);
3085 =sub _items_credits_total
3087 Return the total of al items from _items_credits
3088 Will vary based on invoice display conf flag
3092 sub _items_credits_total {
3095 $tot += $_->{amount} for $self->_items_credits();
3101 =sub _items_credits_postbill()
3103 Returns an array of hashrefs for credits where
3104 - Credit issued after this invoice
3105 - Credit applied to an invoice before this invoice
3107 Returned hashrefs are of the format returned by _items_credits()
3111 sub _items_credits_postbill {
3114 my @cust_credit_bill = qsearch({
3115 table => 'cust_credit_bill',
3116 select => join(', ',qw(
3117 cust_credit_bill.creditbillnum
3118 cust_credit_bill._date
3119 cust_credit_bill.invnum
3120 cust_credit_bill.amount
3122 addl_from => ' LEFT JOIN cust_credit'.
3123 ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
3124 extra_sql => ' WHERE cust_credit.custnum = '.$self->custnum.
3125 ' AND cust_credit_bill._date > '.$self->_date.
3126 ' AND cust_credit_bill.invnum < '.$self->invnum.' ',
3127 #! did not investigate why hashref doesn't work for this join query
3129 # 'cust_credit.custnum' => {op => '=', value => $self->custnum},
3130 # 'cust_credit_bill._date' => {op => '>', value => $self->_date},
3131 # 'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
3137 invnum => $_->invnum,
3138 amount => $_->amount,
3139 creditbillnum => $_->creditbillnum,
3140 }} @cust_credit_bill;
3143 =sub _items_payments_postbill()
3145 Returns an array of hashrefs for payments where
3146 - Payment occured after this invoice
3147 - Payment applied to an invoice before this invoice
3149 Returned hashrefs are of the format returned by _items_payments()
3153 sub _items_payments_postbill {
3156 my @cust_bill_pay = qsearch({
3157 table => 'cust_bill_pay',
3158 select => join(', ',qw(
3159 cust_bill_pay.billpaynum
3161 cust_bill_pay.invnum
3162 cust_bill_pay.amount
3164 addl_from => ' LEFT JOIN cust_bill'.
3165 ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
3166 extra_sql => ' WHERE cust_bill.custnum = '.$self->custnum.
3167 ' AND cust_bill_pay._date > '.$self->_date.
3168 ' AND cust_bill_pay.invnum < '.$self->invnum.' ',
3173 invnum => $_->invnum,
3174 amount => $_->amount,
3175 billpaynum => $_->billpaynum,
3179 =sub _items_payments()
3181 Return array of hashrefs containing payments to be shown as line-items
3182 when rendering this bill.
3184 keys for each payment item:
3185 - paynum: id of payment
3186 - amount: payment amount
3187 - description: line item to be displayed on the bill
3189 This method has three ways it selects which payments to display on
3192 1) Default Case: No Conf flag for 'previous_balance-payments_since'
3194 Returns payments that have been applied to this bill only
3197 Conf flag set for 'previous_balance-payments_since'
3199 List all payments that have been recorded between the timestamps
3200 of the previous invoice and this invoice
3203 Conf flag set for 'previous_balance-payments_since'
3204 $opt{'template'} eq 'statement'
3206 List all payments that have been recorded between the timestamps
3207 of the previous invoice and the following invoice.
3209 I am concerned with this implementation, but leaving in place as is
3210 If this option is selected, while viewing an older bill, the old bill
3211 will show ALL future payments for future bills, but no charges for
3212 future bills. Somebody could be misled into believing they have a
3213 large account credit when they don't. Also, interrupts the chain of
3214 invoices as an account history... the customer could have two invoices
3215 in their fileing cabinet, for two different dates, both with a line item
3216 for the same duplicate payment. The accounting is technically accurate,
3217 but somebody could easily become confused and think two payments were
3218 made, when really those two line items on two different bills represent
3219 only a single payment.
3223 sub _items_payments {
3228 return @{$self->get('_items_payments')} if $self->get('_items_payments');
3231 my $template = $opt{template} || $self->get('_template');
3236 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3238 if ($self->conf->exists('previous_balance-payments_since')) {
3239 if ($template eq 'statement') {
3241 # Case 3 (see above)
3242 # Return payments timestamped between the previous and following bills
3244 my $previous_bill = $self->previous_bill;
3245 my $following_bill = $self->following_bill;
3247 my $date_start = ref $previous_bill ? $previous_bill->_date : 0;
3248 my $date_end = ref $following_bill ? $following_bill->_date : undef;
3251 table => 'cust_pay',
3253 custnum => $self->custnum,
3254 _date => { op => '>=', value => $date_start },
3257 $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3259 @cust_pay_objs = qsearch(\%query);
3262 # Case 2 (see above)
3263 # Return payments timestamped between this and the previous bill
3266 my $date_end = $self->_date;
3268 my $previous_bill = $self->previous_bill;
3269 if (ref $previous_bill) {
3270 $date_start = $previous_bill->_date;
3273 @cust_pay_objs = qsearch({
3274 table => 'cust_pay',
3276 custnum => $self->custnum,
3277 _date => {op => '>=', value => $date_start},
3279 extra_sql => " AND _date <= $date_end ",
3284 # Case 1 (see above)
3285 # Return payments applied only to this bill
3287 @cust_pay_objs = $self->cust_bill_pay;
3293 [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
3295 return @{ $self->get('_items_payments') };
3298 =sub _items_payments_total
3300 Return a total of all records returned by _items_payments
3301 Results vary based on invoicing conf flags
3305 sub _items_payments_total {
3308 $tot += $_->{amount} for $self->_items_payments();
3312 sub __items_payments_make_hashref {
3313 # Transform a FS::cust_pay object into a simple hashref for invoice
3314 my ($self, @cust_pay_objs) = @_;
3315 my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3318 for my $obj (@cust_pay_objs) {
3320 # In case we're passed FS::cust_bill_pay (or something else?)
3321 # Below, we use $obj to render amount rather than $cust_apy.
3322 # If we were passed cust_bill_pay objs, then:
3323 # $obj->amount represents the amount applied to THIS invoice
3324 # $cust_pay->amount represents the total payment, which may have
3325 # been applied accross several invoices.
3326 # If we were passed cust_bill_pay objects, then the conf flag
3327 # previous_balance-payments_since is NOT set, so we should not
3328 # present any payments not applied to this invoice.
3329 my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3332 _date => $cust_pay->_date,
3333 amount => sprintf("%.2f", $obj->amount),
3334 paynum => $cust_pay->paynum,
3335 payinfo => $cust_pay->payby_payinfo_pretty(),
3336 description => join(' ',
3337 $self->mt('Payment received'),
3338 $self->time2str_local('short', $cust_pay->_date),
3342 if ($c_invoice_payment_details) {
3343 $r_obj{description} = join(' ',
3344 $r_obj{description},
3346 $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
3350 push @return, \%r_obj;
3357 Generate the line-items to be shown on the bill in the "Totals" section
3359 Returns a list of hashrefs, each with the keys:
3360 - total_item: description field
3361 - total_amount: dollar-formatted number amount
3363 Information presented by this method varies based on Conf
3365 Conf previous_balance-payments_due
3366 - default, flag not set
3367 Only transactions that were applied to this bill bill be
3368 displayed and calculated intothe total. If items exist in
3369 the past-due section, those items will disappear from this
3370 invoice if they have been paid off.
3372 - previous_balance-payments_due flag is set
3373 Transactions occuring after the timestsamp of this
3374 invoice are not reflected on invoice line items
3376 Only payments/credits applied between the previous invoice
3377 and this one are displayed and calculated into the total
3379 - previous_balance-payments_due && $opt{template} eq 'statement'
3380 Same as above, except payments/credits occuring before the date
3381 of the following invoice are also displayed and calculated into
3384 Conf previous_balance-exclude_from_total
3385 - default, flag not set
3386 The "Totals" section contains a single line item.
3387 The dollar amount of this line items is a sum of old and new charges
3388 - previous_balance-exclude_from_total flag is set
3389 The "Totals" section contains two line items.
3390 One for previous balance, one for new charges
3391 !NOTE: Avent virtualization flag 'disable_previous_balance' can
3392 override the global conf flag previous_balance-exclude_from_total
3394 Conf invoice_show_prior_due_date
3395 - default, flag not set
3396 Total line item in the "Totals" section does not mention due date
3397 - invoice_show_prior_due_date flag is set
3398 Total line item in the "Totals" section includes either the due
3399 date of the invoice, or the specified invoice terms
3400 ? Not sure why this is called "Prior" due date, since we seem to be
3401 displaying THIS due date...
3406 my $conf = $self->conf;
3408 my $c_multi_line_total = 0;
3409 $c_multi_line_total = 1
3410 if $conf->exists('previous_balance-exclude_from_total')
3411 && $self->enable_previous();
3414 my $invoice_charges = $self->charged();
3416 # _items_previous() is aware of conf flags
3417 my $previous_balance = 0;
3418 $previous_balance += $_->{amount} for $self->_items_previous();
3423 if ( $previous_balance && $c_multi_line_total ) {
3424 # previous balance, new charges on separate lines
3427 total_amount => sprintf('%.2f',$previous_balance),
3428 total_item => $self->mt(
3429 $conf->config('previous_balance-text') || 'Previous Balance'
3433 $total_charges = $invoice_charges;
3434 $total_descr = $self->mt(
3435 $conf->config('previous_balance-text-total_new_charges')
3436 || 'Total New Charges'
3440 # previous balance and new charges combined into a single total line
3441 $total_charges = $invoice_charges + $previous_balance;
3442 $total_descr = $self->mt('Total Charges');
3445 if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
3446 # then the due date should be shown with Total New Charges,
3447 # and should NOT be shown with the Balance Due message.
3449 if ( $self->due_date ) {
3450 $total_descr .= $self->invoice_pay_by_msg;
3451 } elsif ( $self->terms ) {
3452 $total_descr = join(' ',
3455 $self->mt($self->terms)
3461 total_amount => sprintf('%.2f', $total_charges),
3462 total_item => $total_descr,
3468 =item _items_aging_balances
3470 Returns an array of aged balance amounts from a given epoch timestamp.
3472 The time of day is ignored for this calculation, so that slight differences
3473 on the generation time of an invoice doesn't determine which column an
3474 aged balance falls into.
3476 Will not include any balances dated after the given timestamp in
3477 the calculated totals
3480 @aged_balances = $b->_items_aging_balances( $b->_date )
3491 sub _items_aging_balances {
3492 my ($self, $basetime) = @_;
3493 die "Incorrect usage of _items_aging_balances()" unless ref $self;
3495 $basetime = $self->_date unless $basetime;
3496 my @aging_balances = (0, 0, 0, 0);
3497 my @open_invoices = $self->_items_previous();
3498 my $d30 = 2592000; # 60 * 60 * 24 * 30,
3499 my $d60 = 5184000; # 60 * 60 * 24 * 60,
3500 my $d90 = 7776000; # 60 * 60 * 24 * 90
3502 # Move the clock back on our given day to 12:00:01 AM
3503 my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
3504 my $dt_12am = DateTime->new(
3505 year => $dt_basetime->year,
3506 month => $dt_basetime->month,
3507 day => $dt_basetime->day,
3513 # set our epoch breakpoints
3514 $_ = $dt_12am - $_ for $d30, $d60, $d90;
3516 # grep the aged balances
3517 for my $oinv (@open_invoices) {
3518 if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
3519 # If post invoice dated less than 30days ago
3520 $aging_balances[0] += $oinv->{amount};
3521 } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
3522 # If past invoice dated between 30-60 days ago
3523 $aging_balances[1] += $oinv->{amount};
3524 } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
3525 # If past invoice dated between 60-90 days ago
3526 $aging_balances[2] += $oinv->{amount};
3528 # If past invoice dated 90+ days ago
3529 $aging_balances[3] += $oinv->{amount};
3533 return map{ sprintf('%.2f',$_) } @aging_balances;
3536 =item has_call_details
3538 Returns true if this invoice has call details.
3542 sub has_call_details {
3545 SELECT 1 FROM cust_bill_pkg_detail
3546 LEFT JOIN cust_bill_pkg USING (billpkgnum)
3547 WHERE cust_bill_pkg_detail.format = 'C'
3548 AND cust_bill_pkg.invnum = ?
3553 =item call_details [ OPTION => VALUE ... ]
3555 Returns an array of CSV strings representing the call details for this invoice
3556 The only option available is the boolean prepend_billed_number
3561 my ($self, %opt) = @_;
3563 my $format_function = sub { shift };
3565 if ($opt{prepend_billed_number}) {
3566 $format_function = sub {
3570 $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3575 my @details = map { $_->details( 'format_function' => $format_function,
3576 'escape_function' => sub{ return() },
3580 $self->cust_bill_pkg;
3581 my $header = $details[0];
3582 ( $header, grep { $_ ne $header } @details );
3585 =item cust_pay_batch
3587 Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
3592 sub cust_pay_batch {
3593 carp "FS::cust_bill->cust_pay_batch is deprecated";
3595 qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
3604 =item process_reprint
3608 sub process_reprint {
3609 process_re_X('print', @_);
3612 =item process_reemail
3616 sub process_reemail {
3617 process_re_X('email', @_);
3625 process_re_X('fax', @_);
3633 process_re_X('ftp', @_);
3640 sub process_respool {
3641 process_re_X('spool', @_);
3646 my( $method, $job ) = ( shift, shift );
3647 warn "$me process_re_X $method for job $job\n" if $DEBUG;
3650 warn Dumper($param) if $DEBUG;
3660 # this is called from search/cust_bill.html and given all its search
3661 # parameters, so it needs to perform the same search.
3664 # spool_invoice ftp_invoice fax_invoice print_invoice
3665 my($method, $job, %param ) = @_;
3667 warn "re_X $method for job $job with param:\n".
3668 join( '', map { " $_ => ". $param{$_}. "\n" } keys %param );
3671 #some false laziness w/search/cust_bill.html
3672 $param{'order_by'} = 'cust_bill._date';
3674 my $query = FS::cust_bill->search(\%param);
3675 delete $query->{'count_query'};
3676 delete $query->{'count_addl'};
3678 $query->{debug} = 1; # was in here before, is obviously useful
3680 my @cust_bill = qsearch( $query );
3682 $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3684 warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3687 my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3688 foreach my $cust_bill ( @cust_bill ) {
3689 $cust_bill->$method();
3691 if ( $job ) { #progressbar foo
3693 if ( time - $min_sec > $last ) {
3694 my $error = $job->update_statustext(
3695 int( 100 * $num / scalar(@cust_bill) )
3697 die $error if $error;
3708 +{ ( map { $_=>$self->$_ } $self->fields ),
3709 'owed' => $self->owed,
3710 #XXX last payment applied date
3716 =head1 CLASS METHODS
3722 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3727 my ($class, $start, $end) = @_;
3729 $class->paid_sql($start, $end). ' - '.
3730 $class->credited_sql($start, $end);
3735 Returns an SQL fragment to retreive the net amount (charged minus credited).
3740 my ($class, $start, $end) = @_;
3741 'charged - '. $class->credited_sql($start, $end);
3746 Returns an SQL fragment to retreive the amount paid against this invoice.
3751 my ($class, $start, $end) = @_;
3752 $start &&= "AND cust_bill_pay._date <= $start";
3753 $end &&= "AND cust_bill_pay._date > $end";
3754 $start = '' unless defined($start);
3755 $end = '' unless defined($end);
3756 "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3757 WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end )";
3762 Returns an SQL fragment to retreive the amount credited against this invoice.
3767 my ($class, $start, $end) = @_;
3768 $start &&= "AND cust_credit_bill._date <= $start";
3769 $end &&= "AND cust_credit_bill._date > $end";
3770 $start = '' unless defined($start);
3771 $end = '' unless defined($end);
3772 "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3773 WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end )";
3778 Returns an SQL fragment to retrieve the due date of an invoice.
3779 Currently only supported on PostgreSQL.
3784 die "don't use: doesn't account for agent-specific invoice_default_terms";
3786 #we're passed a $conf but not a specific customer (that's in the query), so
3787 # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3788 # "condition_sql_option" that retreives a conf value with SQL in an agent-
3791 my $conf = new FS::Conf;
3795 cust_bill.invoice_terms,
3796 cust_main.invoice_terms,
3797 \''.($conf->config('invoice_default_terms') || '').'\'
3798 ), E\'Net (\\\\d+)\'
3800 ) * 86400 + cust_bill._date'
3811 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3812 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base