use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
use strict;
-use vars qw( $DEBUG $me $date_format );
+use vars qw( $DEBUG $me );
# but NOT $conf
use Fcntl qw(:flock); #for spool_csv
use Cwd;
$DEBUG = 0;
$me = '[FS::cust_bill]';
-#ask FS::UID to run this stuff for us later
-FS::UID->install_callback( sub {
- my $conf = new FS::Conf; #global
- $date_format = $conf->config('date_format') || '%x'; #/YY
-} );
-
=head1 NAME
FS::cust_bill - Object methods for cust_bill records
=back
-Customer info at invoice generation time
+Deprecated fields
=over 4
-=item billing_balance - the customer's balance at the time the invoice was
-generated (not including charges on this invoice)
-
-=item previous_balance - the billing_balance of this customer's previous
-invoice plus the charges on that invoice
-
-=back
-
-Deprecated
+=item billing_balance - the customer's balance immediately before generating
+this invoice. DEPRECATED. Use the L<FS::cust_main/balance_date> method
+to determine the customer's balance at a specific time.
-=over 4
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one. DEPRECATED.
-=item printed - deprecated
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
=back
sub display_invnum {
my $self = shift;
- my $conf = $self->conf;
- if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+ if ( $self->agent_invid
+ && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
return $self->agent_invid;
} else {
return $self->invnum;
}
}
-=item cust_credit
-
-Depreciated. See the cust_credited method.
-
- #Returns a list consisting of the total previous credited (see
- #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
- #outstanding credits (FS::cust_credit objects).
-
-=cut
-
-sub cust_credit {
- use Carp;
- croak "FS::cust_bill->cust_credit depreciated; see ".
- "FS::cust_bill->cust_credit_bill";
- #my $self = shift;
- #my $total = 0;
- #my @cust_credit = sort { $a->_date <=> $b->_date }
- # grep { $_->credited != 0 && $_->_date < $self->_date }
- # qsearch('cust_credit', { 'custnum' => $self->custnum } )
- #;
- #foreach (@cust_credit) { $total += $_->credited; }
- #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated. See the cust_bill_pay method.
-
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
-
-=cut
-
-sub cust_pay {
- use Carp;
- croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
- #my $self = shift;
- #sort { $a->_date <=> $b->_date }
- # qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
- #;
-}
-
=item cust_bill_pay
Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
my %return = (
'from' => $args{'from'},
'subject' => ($args{'subject'} || $self->email_subject),
+ 'custnum' => $self->custnum,
+ 'msgtype' => 'invoice',
);
$args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
$alternative->attach(
'Type' => 'text/plain',
'Encoding' => 'quoted-printable',
+ 'Charset' => 'UTF-8',
#'Encoding' => '7bit',
'Data' => $data,
'Disposition' => 'inline',
Place this invoice into the open batch (see C<FS::bill_batch>). If there
isn't an open batch, one will be created.
+HASHREF may contain any options to be passed to C<print_pdf>.
+
=cut
sub batch_invoice {
my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
mkdir $spooldir, 0700 unless -d $spooldir;
+ # don't localize dates here, they're a defined format
my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
my $file = "$spooldir/$tracctnum.csv";
my $taxtotal = 0;
$taxtotal += $_->{'amount'} foreach $self->_items_tax;
- my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+ my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
my( $previous_balance, @unused ) = $self->previous; #previous balance
my $pmt_cr_applied = 0;
$pmt_cr_applied += $_->{'amount'}
- foreach ( $self->_items_payments, $self->_items_credits ) ;
+ foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
? time2str("%x", $cust_bill_pkg->sdate)
: '' ),
($cust_bill_pkg->edate
- ?time2str("%x", $cust_bill_pkg->edate)
+ ? time2str("%x", $cust_bill_pkg->edate)
: '' ),
);
=item invnum_date_pretty
Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
+"Invoice #54 (3/20/2008)".
+
+Intended for back-end context, with regard to translation and date formatting.
=cut
+#note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
+# for backend use (and also does the wrong thing, localizing for end customer
+# instead of backoffice configured date format)
sub invnum_date_pretty {
my $self = shift;
- $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
+ #$self->mt('Invoice #').
+ 'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+ $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
}
#sub _items_extra_usage_sections {
}
+=sub _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this
+invoice. Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
+ my $self = shift;
+ my %opt = @_;
+
+ my $escape = $opt{escape} || sub { $_[0] };
+ my $invnum = $self->invnum;
+ my @classes = qsearch({
+ 'table' => 'usage_class',
+ 'select' => 'classnum, classname, SUM(amount) AS amount',
+ 'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+ ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+ 'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+ ' GROUP BY classnum, classname, weight'.
+ ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+ ' ORDER BY weight ASC',
+ });
+ my @l;
+ my $section = {
+ description => &{$escape}($self->mt('Usage Summary')),
+ no_subtotal => 1,
+ usage_section => 1,
+ };
+ foreach my $class (@classes) {
+ push @l, {
+ 'description' => &{$escape}($class->classname),
+ 'amount' => sprintf('%.2f', $class->amount),
+ 'usage_classnum' => $class->classnum,
+ 'section' => $section,
+ };
+ }
+ return @l;
+}
+
sub _items_previous {
my $self = shift;
my $conf = $self->conf;
my @b = ();
foreach ( @pr_cust_bill ) {
my $date = $conf->exists('invoice_show_prior_due_date')
- ? 'due '. $_->due_date2str($date_format)
- : time2str($date_format, $_->_date);
+ ? 'due '. $_->due_date2str('short')
+ : $self->time2str_local('short', $_->_date);
push @b, {
'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
#'pkgpart' => 'N/A',
#credits
my @objects;
if ( $self->conf->exists('previous_balance-payments_since') ) {
- my $date = 0;
- $date = $self->previous_bill->_date if $self->previous_bill;
- @objects = qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- '_date' => {op => '>=', value => $date},
+ if ( $opt{'template'} eq 'statement' ) {
+ # then the current bill is a "statement" (i.e. an invoice sent as
+ # a payment receipt)
+ # and in that case we want to see payments on or after THIS invoice
+ @objects = qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $self->_date},
+ });
+ } else {
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_credit', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $date},
});
- # hard to do this in the qsearch...
- @objects = grep { $_->_date < $self->_date } @objects;
+ }
} else {
@objects = $self->cust_credited;
}
# " (". time2str("%x",$_->cust_credit->_date) .")".
# $reason,
'description' => $self->mt('Credit applied').' '.
- time2str($date_format,$obj->_date). $reason,
+ $self->time2str_local('short', $obj->_date). $reason,
'amount' => sprintf("%.2f",$obj->amount),
};
}
sub _items_payments {
my $self = shift;
+ my %opt = @_;
my @b;
my $detailed = $self->conf->exists('invoice_payment_details');
my @objects;
if ( $self->conf->exists('previous_balance-payments_since') ) {
- my $date = 0;
- $date = $self->previous_bill->_date if $self->previous_bill;
- @objects = qsearch('cust_pay', {
+ # then show payments dated on/after the previous bill...
+ if ( $opt{'template'} eq 'statement' ) {
+ # then the current bill is a "statement" (i.e. an invoice sent as
+ # a payment receipt)
+ # and in that case we want to see payments on or after THIS invoice
+ @objects = qsearch('cust_pay', {
+ 'custnum' => $self->custnum,
+ '_date' => {op => '>=', value => $self->_date},
+ });
+ } else {
+ # the normal case: payments on or after the previous invoice
+ my $date = 0;
+ $date = $self->previous_bill->_date if $self->previous_bill;
+ @objects = qsearch('cust_pay', {
'custnum' => $self->custnum,
'_date' => {op => '>=', value => $date},
});
- @objects = grep { $_->_date < $self->_date } @objects;
+ # and before the current bill...
+ @objects = grep { $_->_date < $self->_date } @objects;
+ }
} else {
@objects = $self->cust_bill_pay;
}
foreach my $obj (@objects) {
my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
my $desc = $self->mt('Payment received').' '.
- time2str($date_format, $cust_pay->_date );
+ $self->time2str_local('short', $cust_pay->_date );
$desc .= $self->mt(' via ') .
$cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
if $detailed;
}
+sub API_getinfo {
+ my $self = shift;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+ 'owed' => $self->owed,
+ #XXX last payment applied date
+ };
+}
+
=back
=head1 CLASS METHODS
=item newest_percust
+=item custnum
+
+Return only invoices belonging to that customer.
+
+=item cust_classnum
+
+Limit to that customer class (single value or arrayref).
+
+=item payby
+
+Limit to customers with that payment method (single value or arrayref).
+
+=item refnum
+
+Limit to customers with that advertising source.
+
=back
Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
}
+ #payby
+ if ( $param->{payby} ) {
+ my $payby = $param->{payby};
+ $payby = [ $payby ] unless ref $payby;
+ my $payby_in = join(',', map {dbh->quote($_)} @$payby);
+ push @search, "cust_main.payby IN($payby_in)" if length($payby_in);
+ }
+
#_date
if ( $param->{_date} ) {
my($beginning, $ending) = @{$param->{_date}};