summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/Report/FCC_477.pm94
-rw-r--r--FS/FS/Report/Table.pm22
-rw-r--r--FS/FS/Report/Tax.pm79
-rw-r--r--FS/FS/Template_Mixin.pm44
-rw-r--r--FS/FS/cust_pkg/Search.pm3
-rw-r--r--FS/FS/discount.pm4
-rw-r--r--FS/FS/part_event/Condition/signupdate_age.pm1
7 files changed, 186 insertions, 61 deletions
diff --git a/FS/FS/Report/FCC_477.pm b/FS/FS/Report/FCC_477.pm
index 20d402d7d..af45b2dac 100644
--- a/FS/FS/Report/FCC_477.pm
+++ b/FS/FS/Report/FCC_477.pm
@@ -265,7 +265,8 @@ sub active_on {
# "suspended as of some past date" is a complicated query.)
my $date = shift;
"cust_pkg.setup <= $date AND ".
- "(cust_pkg.cancel IS NULL OR cust_pkg.cancel > $date)";
+ "(cust_pkg.cancel IS NULL OR cust_pkg.cancel > $date) AND ".
+ "(cust_pkg.change_date IS NULL OR cust_pkg.change_date <= $date)"
}
sub is_fixed_broadband {
@@ -276,28 +277,68 @@ sub is_mobile_broadband {
"is_broadband::int = 1 AND technology::int IN( 80, 81, 82, 83, 84, 85, 86, 87, 88)"
}
+
=item report SECTION, OPTIONS
Returns the report section SECTION (see the C<parts> method for section
-name strings) as an arrayref of arrayrefs. OPTIONS may contain the following:
+name strings). OPTIONS may contain the following:
- date: a timestamp value. Packages that were active on that date will be
counted.
- agentnum: limit to packages with this agent.
-- detail: if true, the report will contain an additional column which contains
-the keys of all objects aggregated in the row.
-
- ignore_quantity: if true, package quantities will be ignored (only distinct
packages will be counted).
+The result will be a hashref containing three parallel arrayrefs:
+- "data", the columns required by the FCC.
+- "detail", a list of the package numbers included in each row's aggregation
+- "error", a hashref containing any error status strings in that row. Keys
+are error identifiers, values are the messages to show the user.
+as well as an informational item:
+- "num_errors", the number of rows that contain errors
+
+=item report_data SECTION, OPTIONS
+
+Returns only the data, not the detail or error columns. This is the part that
+will be submitted to the FCC.
+
=cut
sub report {
my $class = shift;
my $section = shift;
my %opt = @_;
+ $opt{detail} = 1;
+
+ # add the error column
+ my $data = $class->report_data($section, %opt);
+ my $error = [];
+ my $detail = [];
+ my $check_method = $section.'_check';
+ my $num_errors = 0;
+ foreach my $row (@$data) {
+ if ( $class->can($check_method) ) { # they don't all have these
+ my $eh = $class->$check_method( $row );
+ $num_errors++ if keys(%$eh);
+ push $error, $eh
+ }
+ push @$detail, pop @$row; # this comes from the query
+ }
+
+ return +{
+ data => $data,
+ error => $error,
+ detail => $detail,
+ num_errors => $num_errors,
+ };
+}
+
+sub report_data {
+ my $class = shift;
+ my $section = shift;
+ my %opt = @_;
my $method = $section.'_sql';
die "Report section '$section' is not implemented\n"
@@ -307,7 +348,7 @@ sub report {
warn $statement if $DEBUG;
my $sth = dbh->prepare($statement);
$sth->execute or die $sth->errstr;
- $sth->fetchall_arrayref;
+ return $sth->fetchall_arrayref;
}
sub fbd_sql {
@@ -395,6 +436,34 @@ sub fbs_sql {
}
+sub fbs_check {
+ my $class = shift;
+ my $row = shift;
+ my %e;
+ #censustract
+ if ( length($row->[0]) == 0 ) {
+ $e{'censustract_null'} = 'The package location has no census tract.';
+ } elsif ($row->[0] !~ /^\d{11}$/) {
+ $e{'censustract_bad'} = 'The census tract must be exactly 11 digits.';
+ }
+
+ #technology
+ if ( length($row->[1]) == 0 ) {
+ $e{'technology_null'} = 'The package has no technology type.';
+ }
+
+ #speeds
+ if ( length($row->[2]) == 0 or length($row->[3]) == 0 ) {
+ $e{'speed_null'} = 'The package is missing downstream or upstream speeds.';
+ } elsif ( $row->[2] !~ /^\d*(\.\d+)?$/ or $row->[3] !~ /^\d*(\.\d+)?$/ ) {
+ $e{'speed_bad'} = 'The downstream and upstream speeds must be decimal numbers in Mbps.';
+ } elsif ( $row->[2] == 0 or $row->[3] == 0 ) {
+ $e{'speed_zero'} = 'The downstream and upstream speeds cannot be zero.';
+ }
+
+ return \%e;
+}
+
sub fvs_sql {
my $class = shift;
my %opt = @_;
@@ -440,6 +509,19 @@ sub fvs_sql {
}
+sub fvs_check {
+ my $class = shift;
+ my $row = shift;
+ my %e;
+ #censustract
+ if ( length($row->[0]) == 0 ) {
+ $e{'censustract_null'} = 'The package location has no census tract.';
+ } elsif ($row->[0] !~ /^\d{11}$/) {
+ $e{'censustract_bad'} = 'The census tract must be exactly 11 digits.';
+ }
+ return \%e;
+}
+
sub lts_sql {
my $class = shift;
my %opt = @_;
diff --git a/FS/FS/Report/Table.pm b/FS/FS/Report/Table.pm
index 934287a15..69686df9b 100644
--- a/FS/FS/Report/Table.pm
+++ b/FS/FS/Report/Table.pm
@@ -886,14 +886,24 @@ sub with_classnum {
@$classnum = grep /^\d+$/, @$classnum;
my $in = 'IN ('. join(',', @$classnum). ')';
- my $expr = "
- ( COALESCE(part_pkg.classnum, 0) $in AND pkgpart_override IS NULL)
- OR ( COALESCE(part_fee.classnum, 0) $in AND feepart IS NOT NULL )";
if ( $use_override ) {
- $expr .= "
- OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )";
+ # then include packages if their base package is in the set and they are
+ # not overridden,
+ # or if they are overridden and their override package is in the set,
+ # or fees if they are in the set
+ return "(
+ ( COALESCE(part_pkg.classnum, 0) $in AND cust_pkg.pkgpart IS NOT NULL AND pkgpart_override IS NULL )
+ OR ( COALESCE(override.classnum, 0) $in AND pkgpart_override IS NOT NULL )
+ OR ( COALESCE(part_fee.classnum, 0) $in AND cust_bill_pkg.feepart IS NOT NULL )
+ )";
+ } else {
+ # include packages if their base package is in the set,
+ # or fees if they are in the set
+ return "(
+ ( COALESCE(part_pkg.classnum, 0) $in AND cust_pkg.pkgpart IS NOT NULL )
+ OR ( COALESCE(part_fee.classnum, 0) $in AND cust_bill_pkg.feepart IS NOT NULL )
+ )";
}
- "( $expr )";
}
sub with_usageclass {
diff --git a/FS/FS/Report/Tax.pm b/FS/FS/Report/Tax.pm
index 713be02df..43337a621 100644
--- a/FS/FS/Report/Tax.pm
+++ b/FS/FS/Report/Tax.pm
@@ -95,6 +95,10 @@ sub report_internal {
my $pkg_tax_exempt = "SELECT SUM(amount) AS exempt_charged, billpkgnum, taxnum ".
"FROM cust_tax_exempt_pkg EXEMPT_WHERE GROUP BY billpkgnum, taxnum";
+ # This just calculates the sum of credit applications to a line item.
+ my $pkg_credited = "SELECT SUM(amount) AS credited, billpkgnum ".
+ "FROM cust_credit_bill_pkg GROUP BY billpkgnum";
+
my $where = "WHERE cust_bill._date >= $beginning AND cust_bill._date <= $ending ".
"AND COALESCE(cust_main_county.taxname,'Tax') = '$taxname' ".
"AND cust_main_county.country = '$country'";
@@ -176,25 +180,40 @@ sub report_internal {
$all_sql{exempt_monthly} =~ s/EXEMPT_WHERE/WHERE exempt_monthly = 'Y'/;
# taxable sales
+ # (sale - exemptions - credits, except not negative)
$sql{taxable} = "$select
- SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
- FROM cust_main_county
- JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
- JOIN cust_bill_pkg USING (billpkgnum)
+ SUM(
+ cust_bill_pkg.setup + cust_bill_pkg.recur
+ - COALESCE(exempt_charged, 0)
+ - COALESCE(credited, 0)
+ )
+ FROM cust_bill_pkg
+ LEFT JOIN ($pkg_tax) AS pkg_tax
+ ON (cust_bill_pkg.billpkgnum = pkg_tax.billpkgnum)
LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
- ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum
- AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+ ON (cust_bill_pkg.billpkgnum = pkg_tax_exempt.billpkgnum)
+ LEFT JOIN ($pkg_credited) AS pkg_credited
+ ON (cust_bill_pkg.billpkgnum = pkg_credited.billpkgnum)
+ LEFT JOIN cust_main_county
+ ON (COALESCE(pkg_tax.taxnum, pkg_tax_exempt.taxnum) = cust_main_county.taxnum)
$join_cust_pkg $where AND $nottax
$group";
$all_sql{taxable} = "$select_all
- SUM(cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
- FROM cust_main_county
- JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
- JOIN cust_bill_pkg USING (billpkgnum)
+ SUM(
+ cust_bill_pkg.setup + cust_bill_pkg.recur
+ - COALESCE(exempt_charged, 0)
+ - COALESCE(credited, 0)
+ )
+ FROM cust_bill_pkg
+ LEFT JOIN ($pkg_tax) AS pkg_tax
+ ON (cust_bill_pkg.billpkgnum = pkg_tax.billpkgnum)
LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
- ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum
- AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+ ON (cust_bill_pkg.billpkgnum = pkg_tax_exempt.billpkgnum)
+ LEFT JOIN ($pkg_credited) AS pkg_credited
+ ON (cust_bill_pkg.billpkgnum = pkg_credited.billpkgnum)
+ LEFT JOIN cust_main_county
+ ON (COALESCE(pkg_tax.taxnum, pkg_tax_exempt.taxnum) = cust_main_county.taxnum)
$join_cust_pkg $where AND $nottax
$group_all";
@@ -204,27 +223,39 @@ sub report_internal {
# estimated tax (taxable * rate)
$sql{estimated} = "$select
SUM(cust_main_county.tax / 100 *
- (cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
+ ( cust_bill_pkg.setup + cust_bill_pkg.recur
+ - COALESCE(exempt_charged, 0)
+ - COALESCE(credited, 0)
+ )
)
- FROM cust_main_county
- JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
- JOIN cust_bill_pkg USING (billpkgnum)
+ FROM cust_bill_pkg
+ LEFT JOIN ($pkg_tax) AS pkg_tax
+ ON (cust_bill_pkg.billpkgnum = pkg_tax.billpkgnum)
LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
- ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum
- AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+ ON (cust_bill_pkg.billpkgnum = pkg_tax_exempt.billpkgnum)
+ LEFT JOIN ($pkg_credited) AS pkg_credited
+ ON (cust_bill_pkg.billpkgnum = pkg_credited.billpkgnum)
+ LEFT JOIN cust_main_county
+ ON (COALESCE(pkg_tax.taxnum, pkg_tax_exempt.taxnum) = cust_main_county.taxnum)
$join_cust_pkg $where AND $nottax
$group";
$all_sql{estimated} = "$select_all
SUM(cust_main_county.tax / 100 *
- (cust_bill_pkg.setup + cust_bill_pkg.recur - COALESCE(exempt_charged, 0))
+ ( cust_bill_pkg.setup + cust_bill_pkg.recur
+ - COALESCE(exempt_charged, 0)
+ - COALESCE(credited, 0)
+ )
)
- FROM cust_main_county
- JOIN ($pkg_tax) AS pkg_tax USING (taxnum)
- JOIN cust_bill_pkg USING (billpkgnum)
+ FROM cust_bill_pkg
+ LEFT JOIN ($pkg_tax) AS pkg_tax
+ ON (cust_bill_pkg.billpkgnum = pkg_tax.billpkgnum)
LEFT JOIN ($pkg_tax_exempt) AS pkg_tax_exempt
- ON (pkg_tax_exempt.billpkgnum = cust_bill_pkg.billpkgnum
- AND pkg_tax_exempt.taxnum = cust_main_county.taxnum)
+ ON (cust_bill_pkg.billpkgnum = pkg_tax_exempt.billpkgnum)
+ LEFT JOIN ($pkg_credited) AS pkg_credited
+ ON (cust_bill_pkg.billpkgnum = pkg_credited.billpkgnum)
+ LEFT JOIN cust_main_county
+ ON (COALESCE(pkg_tax.taxnum, pkg_tax_exempt.taxnum) = cust_main_county.taxnum)
$join_cust_pkg $where AND $nottax
$group_all";
diff --git a/FS/FS/Template_Mixin.pm b/FS/FS/Template_Mixin.pm
index f67de76fc..ebc977a35 100644
--- a/FS/FS/Template_Mixin.pm
+++ b/FS/FS/Template_Mixin.pm
@@ -346,8 +346,8 @@ sub print_generic {
if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
#change this to a die when the old code is removed
- # it's been almost ten years, changing it to a die.
- die "old-style invoice template $templatefile; ".
+ # it's been almost ten years, changing it to a die on the next release.
+ warn "old-style invoice template $templatefile; ".
"patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
#$old_latex = 'true';
#@invoice_template = _translate_old_latex_format(@invoice_template);
@@ -2638,14 +2638,14 @@ sub _items_cust_bill_pkg {
# and location labels
my @b = (); # accumulator for the line item hashes that we'll return
- my ($s, $r, $u, $d) = ( undef, undef, undef );
+ my ($s, $r, $u, $d) = ( undef, undef, undef, undef );
# the 'current' line item hashes for setup, recur, usage, discount
foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
{
# if the current line item is waiting to go out, and the one we're about
# to start is not bundled, then push out the current one and start a new
# one.
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) , $d ) {
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
if ( $_ && !$cust_bill_pkg->hidden ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} );
$_->{amount} =~ s/^\-0\.00$/0.00/;
@@ -3047,6 +3047,15 @@ sub _items_cust_bill_pkg {
or ( $type eq 'R' and $cust_bill_pkg->unitrecur > 0 )
) {
+ # the line item hashref for the line that will show the original
+ # price
+ # (use the recur or single line for the package, unless we're
+ # showing a setup line for a package with no recurring fee)
+ my $active_line = $r;
+ if ( $type eq 'S' ) {
+ $active_line = $s;
+ }
+
my @discounts = $cust_bill_pkg->cust_bill_pkg_discount;
# special case: if there are old "discount details" on this line
# item, don't show discount line items
@@ -3061,23 +3070,18 @@ sub _items_cust_bill_pkg {
$cust_bill_pkg->billpkgnum."\n"
if $DEBUG;
my $discount_amount = sum( map {$_->amount} @discounts );
- my $orig_amount = $cust_bill_pkg->setup + $cust_bill_pkg->recur
- + $discount_amount;
# if multiple discounts apply to the same package, how to display
# them? ext_description lines, apparently
+ #
+ # # discount amounts are negative
if ( $d and $cust_bill_pkg->hidden ) {
- $d->{amount} += $discount_amount;
- $d->{orig_amount} += $orig_amount;
+ $d->{amount} -= $discount_amount;
} else {
my @ext;
- # make a placeholder for the original price, if necessary
- # (if unit prices are enabled, it won't be necessary)
- push @ext, '' if !$conf->exists('invoice-unitprice');
$d = {
_is_discount => 1,
- description => $self->mt('Discount included'),
- amount => $discount_amount,
- orig_amount => $orig_amount,
+ description => $self->mt('Discount'),
+ amount => -1 * $discount_amount,
ext_description => \@ext,
};
foreach my $cust_bill_pkg_discount (@discounts) {
@@ -3086,12 +3090,10 @@ sub _items_cust_bill_pkg {
}
}
- # update the placeholder to show the original price in the
- # first ext_description line
- if ( !$conf->exists('invoice-unitprice') ) {
- $d->{ext_description}->[0] =
- sprintf('Original price: %.2f', $d->{orig_amount});
- }
+ # update the active line (before the discount) to show the
+ # original price (whether this is a hidden line or not)
+ $active_line->{amount} += $discount_amount;
+
} # if there are any discounts
} # if this is an appropriate place to show discounts
@@ -3116,7 +3118,7 @@ sub _items_cust_bill_pkg {
}
- foreach ( $s, $r, ($opt{skip_usage} ? () : $u, $d ) ) {
+ foreach ( $s, $r, ($opt{skip_usage} ? () : $u ), $d ) {
if ( $_ ) {
$_->{amount} = sprintf( "%.2f", $_->{amount} ),
if exists($_->{amount});
diff --git a/FS/FS/cust_pkg/Search.pm b/FS/FS/cust_pkg/Search.pm
index aacd387a6..89809de6c 100644
--- a/FS/FS/cust_pkg/Search.pm
+++ b/FS/FS/cust_pkg/Search.pm
@@ -585,9 +585,8 @@ sub search {
'agentnum' => $agentnum,
'detail' => 1
);
- my $row = $report->[$rownum]
+ my $pkgnums = $report->{detail}->[$rownum]
or die "row $rownum is past the end of the report";
- my $pkgnums = $row->[-1] || '0';
# '0' so that if there are no pkgnums (empty string) it will create
# a valid query that returns nothing
warn "PKGNUMS:\n$pkgnums\n\n"; # XXX debug
diff --git a/FS/FS/discount.pm b/FS/FS/discount.pm
index 43ad4909e..0561f9ca0 100644
--- a/FS/FS/discount.pm
+++ b/FS/FS/discount.pm
@@ -174,12 +174,12 @@ sub description_short {
my $money_char = $conf->config('money_char') || '$';
my $desc = $self->name ? $self->name.': ' : '';
- $desc .= $money_char. sprintf('%.2f/month ', $self->amount)
+ $desc .= $money_char. sprintf('%.2f/month', $self->amount)
if $self->amount > 0;
( my $percent = $self->percent ) =~ s/\.0+$//;
$percent =~ s/(\.\d*[1-9])0+$/$1/;
- $desc .= $percent. '% '
+ $desc .= $percent. '%'
if $self->percent > 0;
$desc;
diff --git a/FS/FS/part_event/Condition/signupdate_age.pm b/FS/FS/part_event/Condition/signupdate_age.pm
index 70b4bbd7b..0c78b4c10 100644
--- a/FS/FS/part_event/Condition/signupdate_age.pm
+++ b/FS/FS/part_event/Condition/signupdate_age.pm
@@ -18,6 +18,7 @@ sub condition {
my $age = $self->option_age_from('age', $opt{'time'} );
+ my $cust_main = $cust_bill->cust_main;
( $cust_main->signupdate - 60 ) <= $age;
}