+ my $sql = '';
+ if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
+ $sql .= " and custnum = $1 ";
+ }
+ if ( $opt{'refnum'} ) {
+ my $refnum = $opt{'refnum'};
+ $refnum = [ $refnum ] if !ref($refnum);
+ my $in = join(',', grep /^\d+$/, @$refnum);
+ $sql .= " and refnum IN ($in)" if length $in;
+ }
+ if ( my $where = $self->with_cust_classnum(%opt) ) {
+ $sql .= " and $where";
+ }
+
+ $sql;
+}
+
+sub with_classnum {
+ my ($self, $classnum, $use_override) = @_;
+ return '' if $classnum eq '';
+
+ $classnum = [ $classnum ] if !ref($classnum);
+ @$classnum = grep /^\d+$/, @$classnum;
+ return '' if !@$classnum;
+ my $in = 'IN ('. join(',', @$classnum). ')';
+
+ if ( $use_override ) {
+ # 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 )
+ )";
+ }
+}
+
+sub with_usageclass {
+ my $self = shift;
+ my ($classnum, $use_override) = @_;
+ return '' unless $classnum =~ /^\d+$/;
+ my $comparison;
+ if ( $classnum == 0 ) {
+ $comparison = 'IS NULL';
+ }
+ else {
+ $comparison = "= $classnum";
+ }
+ return "cust_bill_pkg_detail.classnum $comparison";
+}
+
+sub with_report_option {
+ my ($self, %opt) = @_;
+ # %opt can contain:
+ # - report_optionnum: a comma-separated list of numbers. Zero means to
+ # include packages with _no_ report classes.
+ # - not_report_optionnum: a comma-separated list. Packages that have
+ # any of these report options will be excluded from the result.
+ # Zero does nothing.
+ # - use_override: also matches line items that are add-ons to a package
+ # matching the report class.
+ # - all_report_options: returns only packages that have ALL of the
+ # report classes listed in $num. Otherwise, will return packages that
+ # have ANY of those classes.
+
+ my @num = ref($opt{'report_optionnum'})
+ ? @{ $opt{'report_optionnum'} }
+ : split(/\s*,\s*/, $opt{'report_optionnum'});
+ my @not_num = ref($opt{'not_report_optionnum'})
+ ? @{ $opt{'not_report_optionnum'} }
+ : split(/\s*,\s*/, $opt{'not_report_optionnum'});
+ my $null;
+ $null = 1 if ( grep {$_ == 0} @num );
+ @num = grep {$_ > 0} @num;
+ @not_num = grep {$_ > 0} @not_num;
+
+ # brute force
+ my $table = $opt{'use_override'} ? 'override' : 'part_pkg';
+ my $op = ' OR ';
+ if ( $opt{'all_report_options'} ) {
+ if ( @num and $null ) {
+ return 'false'; # mutually exclusive criteria, so just bail out
+ }
+ $op = ' AND ';
+ }
+ my @where_num = map {
+ "EXISTS(SELECT 1 FROM part_pkg_option ".
+ "WHERE optionname = 'report_option_$_' ".
+ "AND part_pkg_option.pkgpart = $table.pkgpart)"
+ } @num;
+ if ( $null ) {
+ push @where_num, "NOT EXISTS(SELECT 1 FROM part_pkg_option ".
+ "WHERE optionname LIKE 'report_option_%' ".
+ "AND part_pkg_option.pkgpart = $table.pkgpart)";
+ }
+ my @where_not_num = map {
+ "NOT EXISTS(SELECT 1 FROM part_pkg_option ".
+ "WHERE optionname = 'report_option_$_' ".
+ "AND part_pkg_option.pkgpart = $table.pkgpart)"
+ } @not_num;
+
+ my @where;
+ if (@where_num) {
+ push @where, '( '.join($op, @where_num).' )';
+ }
+ if (@where_not_num) {
+ push @where, '( '.join(' AND ', @where_not_num).' )';
+ }
+
+ return @where;
+ # this messes up totals
+ #if ( $opt{'use_override'} ) {
+ # # then also allow the non-override package to match
+ # delete $opt{'use_override'};
+ # $comparison = "( $comparison OR " . $self->with_report_option(%opt) . ")";
+ #}
+
+}
+
+sub with_refnum {
+ my ($self, %opt) = @_;
+ if ( $opt{'refnum'} ) {
+ my $refnum = $opt{'refnum'};
+ $refnum = [ $refnum ] if !ref($refnum);
+ my $in = join(',', grep /^\d+$/, @$refnum);
+ return "cust_main.refnum IN ($in)" if length $in;
+ }
+ return;
+}
+
+sub with_towernum {
+ my ($self, %opt) = @_;
+ if ( $opt{'towernum'} ) {
+ my $towernum = $opt{'towernum'};
+ $towernum = [ $towernum ] if !ref($towernum);
+ my $in = join(',', grep /^\d+$/, @$towernum);
+ return unless length($in); # if no towers are specified, don't restrict
+
+ # materialize/cache the set of pkgnums that, as of the last
+ # svc_broadband history record, had a certain towernum
+ # (because otherwise this is painfully slow)
+ $self->_init_tower_pkg_cache;
+
+ return "EXISTS(
+ SELECT 1 FROM tower_pkg_cache
+ WHERE towernum IN($in)
+ AND cust_pkg.pkgnum = tower_pkg_cache.pkgnum
+ )";
+ }
+ return;
+}
+
+sub with_zip {
+ my ($self, %opt) = @_;
+ if (length($opt{'zip'})) {
+ return "(SELECT zip FROM cust_location
+ WHERE cust_location.locationnum = cust_pkg.locationnum
+ ) = " . dbh->quote($opt{'zip'});
+ }
+ return;
+}
+
+sub with_cust_classnum {
+ my ($self, %opt) = @_;
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ return 'cust_main.classnum in('. join(',',@$classnums) .')'
+ if @$classnums;
+ }
+ return;