use FS::cust_bill_pkg_tax_rate_location;
use FS::cust_tax_adjustment;
+use List::Util qw(sum);
+
@ISA = qw( FS::cust_main_Mixin FS::Record );
$DEBUG = 0;
if ( $self->get('details') ) {
foreach my $detail ( @{$self->get('details')} ) {
- my %hash = ();
- if ( ref($detail) ) {
- if ( ref($detail) eq 'ARRAY' ) {
- #carp "this way sucks, use a hash"; #but more useful/friendly
- $hash{'format'} = $detail->[0];
- $hash{'detail'} = $detail->[1];
- $hash{'amount'} = $detail->[2];
- $hash{'classnum'} = $detail->[3];
- $hash{'phonenum'} = $detail->[4];
- $hash{'accountcode'} = $detail->[5];
- $hash{'startdate'} = $detail->[6];
- $hash{'duration'} = $detail->[7];
- $hash{'regionname'} = $detail->[8];
- } elsif ( ref($detail) eq 'HASH' ) {
- %hash = %$detail;
- } else {
- die "unknow detail type ". ref($detail);
- }
- } else {
- $hash{'detail'} = $detail;
- }
- $hash{'billpkgnum'} = $self->billpkgnum;
- my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail \%hash;
- $error = $cust_bill_pkg_detail->insert;
+ $detail->billpkgnum($self->billpkgnum);
+ $error = $detail->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "error inserting cust_bill_pkg_detail: $error";
;
return $error if $error;
+ $self->regularize_details;
+
#if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
return "Unknown pkgnum ". $self->pkgnum
$self->SUPER::check;
}
+=item regularize_details
+
+Converts the contents of the 'details' pseudo-field to
+L<FS::cust_bill_pkg_detail> objects, if they aren't already.
+
+=cut
+
+sub regularize_details {
+ my $self = shift;
+ if ( $self->get('details') ) {
+ foreach my $detail ( @{$self->get('details')} ) {
+ if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
+ # then turn it into one
+ my %hash = ();
+ if ( ! ref($detail) ) {
+ $hash{'detail'} = $detail;
+ }
+ elsif ( ref($detail) eq 'HASH' ) {
+ %hash = %$detail;
+ }
+ elsif ( ref($detail) eq 'ARRAY' ) {
+ carp "passing invoice details as arrays is deprecated";
+ #carp "this way sucks, use a hash"; #but more useful/friendly
+ $hash{'format'} = $detail->[0];
+ $hash{'detail'} = $detail->[1];
+ $hash{'amount'} = $detail->[2];
+ $hash{'classnum'} = $detail->[3];
+ $hash{'phonenum'} = $detail->[4];
+ $hash{'accountcode'} = $detail->[5];
+ $hash{'startdate'} = $detail->[6];
+ $hash{'duration'} = $detail->[7];
+ $hash{'regionname'} = $detail->[8];
+ }
+ else {
+ die "unknown detail type ". ref($detail);
+ }
+ $detail = new FS::cust_bill_pkg_detail \%hash;
+ }
+ $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
+ }
+ }
+ return;
+}
+
=item cust_pkg
Returns the package (see L<FS::cust_pkg>) for this invoice line item.
my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
" WHERE billpkgnum = ". $self->billpkgnum.
- " AND ( format IS NULL OR format != 'C' ".
+ " AND ( format IS NULL OR format != 'C' ) ".
" ORDER BY detailnum";
my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute or die $sth->errstr;
#avoid the fetchall_arrayref and loop for less memory usage?
- map { $_->[0] eq 'C'
+ map { (defined($_->[0]) && $_->[0] eq 'C')
? &{$format_sub}( $_->[1] )
: &{$escape_function}( $_->[1] );
}
sub usage {
my( $self, $classnum ) = @_;
+ $self->regularize_details;
if ( $self->get('details') ) {
- my $sum = 0;
- foreach my $value (
- map { ref($_) eq 'HASH'
- ? $_->{'amount'}
- : $_->[2]
- }
- grep { ref($_) && ( defined($classnum)
- ? $classnum eq ( ref($_) eq 'HASH'
- ? $_->{'classnum'}
- : $_->[3]
- )
- : 1
- )
- }
+ return sum( 0,
+ map { $_->amount || 0 }
+ grep { !defined($classnum) or $classnum eq $_->classnum }
@{ $self->get('details') }
- ) {
- $sum += $value if $value;
- }
-
- return $sum;
+ );
} else {
my $sth = dbh->prepare($sql) or die dbh->errstr;
$sth->execute or die $sth->errstr;
- return $sth->fetchrow_arrayref->[0];
+ return $sth->fetchrow_arrayref->[0] || 0;
}
sub usage_classes {
my( $self ) = @_;
+ $self->regularize_details;
if ( $self->get('details') ) {
- my %seen = ();
- foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
- $seen{ ref($detail) eq 'HASH'
- ? $detail->{'classnum'}
- : $detail->[3]
- } = 1;
- }
+ my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
keys %seen;
} else {
my $default =
new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
- return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
-
my $type = $opt{type} if exists $opt{type};
my @result;
=cut
-sub recur_show_zero {
- #my $self = shift;
- # $self->recur == 0
- #&& $self->pkgnum
- #&& $self->cust_pkg->part_pkg->recur_show_zero;
+sub recur_show_zero { shift->_X_show_zero('recur'); }
+sub setup_show_zero { shift->_X_show_zero('setup'); }
- shift->_X_show_zero('recur');
+sub _X_show_zero {
+ my( $self, $what ) = @_;
+ return 0 unless $self->$what() == 0 && $self->pkgnum;
+
+ $self->cust_pkg->_X_show_zero($what);
}
-sub setup_show_zero {
- shift->_X_show_zero('setup');
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=item usage_sql
+
+Returns an SQL expression for the total usage charges in details on
+an item.
+
+=cut
+
+my $usage_sql =
+ '(SELECT COALESCE(SUM(cust_bill_pkg_detail.amount),0)
+ FROM cust_bill_pkg_detail
+ WHERE cust_bill_pkg_detail.billpkgnum = cust_bill_pkg.billpkgnum)';
+
+sub usage_sql { $usage_sql }
+
+# this makes owed_sql, etc. much more concise
+sub charged_sql {
+ my ($class, $start, $end, %opt) = @_;
+ my $charged =
+ $opt{setuprecur} =~ /^s/ ? 'cust_bill_pkg.setup' :
+ $opt{setuprecur} =~ /^r/ ? 'cust_bill_pkg.recur' :
+ 'cust_bill_pkg.setup + cust_bill_pkg.recur';
+
+ if ($opt{no_usage} and $charged =~ /recur/) {
+ $charged = "$charged - $usage_sql"
+ }
+
+ $charged;
}
-sub _X_show_zero {
- my( $self, $what ) = @_;
- return 0 unless $self->$what() == 0 && $self->pkgnum;
+=item owed_sql [ BEFORE, AFTER, OPTIONS ]
+
+Returns an SQL expression for the amount owed. BEFORE and AFTER specify
+a date window. OPTIONS may include 'no_usage' (excludes usage charges)
+and 'setuprecur' (set to "setup" or "recur" to limit to one or the other).
+
+=cut
+
+sub owed_sql {
+ my $class = shift;
+ '(' . $class->charged_sql(@_) .
+ ' - ' . $class->paid_sql(@_) .
+ ' - ' . $class->credited_sql(@_) . ')'
+}
+
+=item paid_sql [ BEFORE, AFTER, OPTIONS ]
+
+Returns an SQL expression for the sum of payments applied to this item.
+
+=cut
+
+sub paid_sql {
+ my ($class, $start, $end, %opt) = @_;
+ my $s = $start ? "AND cust_bill_pay._date <= $start" : '';
+ my $e = $end ? "AND cust_bill_pay._date > $end" : '';
+ my $setuprecur =
+ $opt{setuprecur} =~ /^s/ ? 'setup' :
+ $opt{setuprecur} =~ /^r/ ? 'recur' :
+ '';
+ $setuprecur &&= "AND setuprecur = '$setuprecur'";
+
+ my $paid = "( SELECT COALESCE(SUM(cust_bill_pay_pkg.amount),0)
+ FROM cust_bill_pay_pkg JOIN cust_bill_pay USING (billpaynum)
+ WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum
+ $s $e$setuprecur )";
+
+ if ( $opt{no_usage} ) {
+ # cap the amount paid at the sum of non-usage charges,
+ # minus the amount credited against non-usage charges
+ "LEAST($paid, ".
+ $class->charged_sql($start, $end, %opt) . ' - ' .
+ $class->credited_sql($start, $end, %opt).')';
+ }
+ else {
+ $paid;
+ }
+
+}
+
+sub credited_sql {
+ my ($class, $start, $end, %opt) = @_;
+ my $s = $start ? "AND cust_credit_bill._date <= $start" : '';
+ my $e = $end ? "AND cust_credit_bill._date > $end" : '';
+ my $setuprecur =
+ $opt{setuprecur} =~ /^s/ ? 'setup' :
+ $opt{setuprecur} =~ /^r/ ? 'recur' :
+ '';
+ $setuprecur &&= "AND setuprecur = '$setuprecur'";
+
+ my $credited = "( SELECT COALESCE(SUM(cust_credit_bill_pkg.amount),0)
+ FROM cust_credit_bill_pkg JOIN cust_credit_bill USING (creditbillnum)
+ WHERE cust_credit_bill_pkg.billpkgnum = cust_bill_pkg.billpkgnum
+ $s $e $setuprecur )";
+
+ if ( $opt{no_usage} ) {
+ # cap the amount credited at the sum of non-usage charges
+ "LEAST($credited, ". $class->charged_sql($start, $end, %opt).')';
+ }
+ else {
+ $credited;
+ }
- $self->cust_pkg->_X_show_zero($what);
}
=back