=over 4
+=item signups: The number of customers signed up. Options are "refnum"
+(limit by advertising source) and "indirect" (boolean, tells us to limit
+to customers that have a referral_custnum that matches the advertising source).
+
+=cut
+
+sub signups {
+ my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
+ my @where = ( $self->in_time_period_and_agent($speriod, $eperiod, $agentnum,
+ 'cust_main.signupdate')
+ );
+ my $join = '';
+ if ( $opt{'indirect'} ) {
+ $join = " JOIN cust_main AS referring_cust_main".
+ " ON (cust_main.referral_custnum = referring_cust_main.custnum)";
+
+ if ( $opt{'refnum'} ) {
+ push @where, "referring_cust_main.refnum = ".$opt{'refnum'};
+ }
+ }
+ elsif ( $opt{'refnum'} ) {
+ push @where, "refnum = ".$opt{'refnum'};
+ }
+
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
+ $self->scalar_sql(
+ "SELECT COUNT(*) FROM cust_main $join WHERE ".join(' AND ', @where)
+ );
+}
+
=item invoiced: The total amount charged on all invoices.
=cut
sub invoiced { #invoiced
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
+ my $sql = 'SELECT SUM(cust_bill.charged) FROM cust_bill';
+ if ( $opt{'setuprecur'} ) {
+ $sql = 'SELECT SUM('.
+ FS::cust_bill_pkg->charged_sql($speriod, $eperiod, %opt).
+ ') FROM cust_bill_pkg JOIN cust_bill USING (invnum)';
+ }
+
$self->scalar_sql("
- SELECT SUM(charged)
- FROM cust_bill
+ $sql
LEFT JOIN cust_main USING ( custnum )
- WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
- . (%opt ? $self->for_custnum(%opt) : '')
+ WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
+ $self->for_opts(%opt)
);
}
sub netsales { #net sales
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
- $self->invoiced($speriod,$eperiod,$agentnum,%opt)
- - $self->netcredits($speriod,$eperiod,$agentnum,%opt);
+ $self->invoiced( $speriod, $eperiod, $agentnum, %opt)
+ - $self->netcredits($speriod, $eperiod, $agentnum, %opt);
}
=item cashflow: payments - refunds
=cut
sub netcashflow {
- my( $self, $speriod, $eperiod, $agentnum ) = @_;
+ my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
- $self->receipts($speriod, $eperiod, $agentnum)
- - $self->netrefunds( $speriod, $eperiod, $agentnum);
+ $self->receipts( $speriod, $eperiod, $agentnum, %opt)
+ - $self->netrefunds( $speriod, $eperiod, $agentnum, %opt);
}
=item payments: The sum of payments received in the period.
SELECT SUM(paid)
FROM cust_pay
LEFT JOIN cust_main USING ( custnum )
- WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
- . (%opt ? $self->for_custnum(%opt) : '')
+ WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
+ $self->for_opts(%opt)
);
}
=cut
sub credits {
- my( $self, $speriod, $eperiod, $agentnum ) = @_;
+ my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
$self->scalar_sql("
SELECT SUM(amount)
FROM cust_credit
LEFT JOIN cust_main USING ( custnum )
- WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
+ WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
+ $self->for_opts(%opt)
);
}
SELECT SUM(refund)
FROM cust_refund
LEFT JOIN cust_main USING ( custnum )
- WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
- . (%opt ? $self->for_custnum(%opt) : '')
+ WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum).
+ $self->for_opts(%opt)
);
}
sub netcredits {
my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
+
+ my $sql = 'SELECT SUM(cust_credit_bill.amount) FROM cust_credit_bill';
+ if ( $opt{'setuprecur'} ) {
+ $sql = 'SELECT SUM('.
+ FS::cust_bill_pkg->credited_sql($speriod, $eperiod, %opt).
+ ') FROM cust_bill_pkg';
+ }
+
$self->scalar_sql("
- SELECT SUM(cust_credit_bill.amount)
- FROM cust_credit_bill
+ $sql
LEFT JOIN cust_bill USING ( invnum )
LEFT JOIN cust_main USING ( custnum )
WHERE ". $self->in_time_period_and_agent( $speriod,
$eperiod,
$agentnum,
'cust_bill._date'
- )
- . (%opt ? $self->for_custnum(%opt) : '')
+ ).
+ $self->for_opts(%opt)
);
}
=cut
sub receipts { #net payments
- my( $self, $speriod, $eperiod, $agentnum ) = @_;
+ my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
+
+ my $sql = 'SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay';
+ if ( $opt{'setuprecur'} ) {
+ $sql = 'SELECT SUM('.
+ FS::cust_bill_pkg->paid_sql($speriod, $eperiod, %opt).
+ ') FROM cust_bill_pkg';
+ }
+
$self->scalar_sql("
- SELECT SUM(cust_bill_pay.amount)
- FROM cust_bill_pay
+ $sql
LEFT JOIN cust_bill USING ( invnum )
LEFT JOIN cust_main USING ( custnum )
WHERE ". $self->in_time_period_and_agent( $speriod,
$eperiod,
$agentnum,
'cust_bill._date'
- )
+ ).
+ $self->for_opts(%opt)
);
}
=cut
sub netrefunds {
- my( $self, $speriod, $eperiod, $agentnum ) = @_;
+ my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
$self->scalar_sql("
SELECT SUM(cust_credit_refund.amount)
FROM cust_credit_refund
$eperiod,
$agentnum,
'cust_credit._date'
- )
+ ).
+ $self->for_opts(%opt)
);
}
$self->in_time_period_and_agent($speriod, $eperiod, $agentnum),
);
+ # yuck, false laziness
+ push @where, "cust_main.refnum = ". $opt{'refnum'} if $opt{'refnum'};
+
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
my $total_sql = "SELECT COALESCE(SUM(cust_bill_pkg.setup),0)
FROM cust_bill_pkg
$cust_bill_pkg_join
$self->with_classnum($opt{'classnum'}, $opt{'use_override'}),
);
+ push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
+
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
# subtract all usage from the line item regardless of date
my $item_usage;
if ( $opt{'project'} ) {
if ( $opt{'distribute'} ) {
push @where, "cust_main.agentnum = $agentnum" if $agentnum;
push @where,
- "$cust_bill_pkg.sdate < $eperiod",
- "$cust_bill_pkg.edate > $speriod",
+ "$cust_bill_pkg.sdate < $eperiod",
+ "$cust_bill_pkg.edate >= $speriod",
;
# the fraction of edate - sdate that's within [speriod, eperiod]
$recur_fraction = " *
my @where = ( "cust_bill_pkg.pkgnum != 0" );
+ push @where, 'cust_main.refnum = '. $opt{'refnum'} if $opt{'refnum'};
+
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ push @where, 'cust_main.classnum in('. join(',',@$classnums) .')';
+ }
+
$agentnum ||= $opt{'agentnum'};
push @where,
}
-sub setup_pkg { shift->pkg_field( @_, 'setup' ); }
-sub susp_pkg { shift->pkg_field( @_, 'susp' ); }
-sub cancel_pkg { shift->pkg_field( @_, 'cancel'); }
+sub setup_pkg { shift->pkg_field( 'setup', @_ ); }
+sub susp_pkg { shift->pkg_field( 'susp', @_ ); }
+sub cancel_pkg { shift->pkg_field( 'cancel', @_ ); }
sub pkg_field {
- my( $self, $speriod, $eperiod, $agentnum, $field ) = @_;
+ my( $self, $field, $speriod, $eperiod, $agentnum ) = @_;
$self->scalar_sql("
SELECT COUNT(*) FROM cust_pkg
LEFT JOIN cust_main USING ( custnum )
$sql;
}
-sub for_custnum {
+sub for_opts {
my ( $self, %opt ) = @_;
- return '' unless $opt{'custnum'};
- $opt{'custnum'} =~ /^\d+$/ ? " and custnum = $opt{custnum} " : '';
+ my $sql = '';
+ if ( $opt{'custnum'} =~ /^(\d+)$/ ) {
+ $sql .= " and custnum = $1 ";
+ }
+ if ( $opt{'refnum'} =~ /^(\d+)$/ ) {
+ $sql .= " and refnum = $1 ";
+ }
+ if ( $opt{'cust_classnum'} ) {
+ my $classnums = $opt{'cust_classnum'};
+ $classnums = [ $classnums ] if !ref($classnums);
+ @$classnums = grep /^\d+$/, @$classnums;
+ $sql .= ' and cust_main.classnum in('. join(',',@$classnums) .')'
+ if @$classnums;
+ }
+
+ $sql;
}
sub with_classnum {
my ($speriod, $eperiod) = @_;
my %items = map {$_ => 1} @{ $self->{items} };
if ($items{'cust_bill_pkg'}) {
- # append, head-to-tail, new line items identical to any that end within the
- # period (and aren't expiring)
+ # What we do here:
+ # Find all line items that end after the start of the period (and have
+ # recurring fees, and don't expire before they end). Choose the latest
+ # one for each package. If it ends before the end of the period, copy
+ # it forward by one billing period.
+ # Repeat this until the latest line item for each package no longer ends
+ # within the period. This is certain to happen in finitely many
+ # iterations as long as freq > 0.
+ # - Pg only, obviously.
+ # - Gives bad results if freq_override is used.
my @fields = ( FS::cust_bill_pkg->fields, qw( usage _date expire ) );
my $insert_fields = join(',', @fields);
- #advance (sdate, edate) by one billing period
+ my $add_freq = sub { # emulate FS::part_pkg::add_freq
+ my $field = shift;
+ "EXTRACT( EPOCH FROM TO_TIMESTAMP($field) + (CASE WHEN freq ~ E'\\\\D' ".
+ "THEN freq ELSE freq || 'mon' END)::INTERVAL) AS $field";
+ };
foreach (@fields) {
if ($_ eq 'edate') {
- $_ = '(edate + (edate - sdate)) AS edate' #careful of integer overflow
+ $_ = $add_freq->('edate');
}
elsif ($_ eq 'sdate') {
$_ = 'edate AS sdate'
$_ = '0 AS setup' #because recurring only
}
elsif ($_ eq '_date') {
- $_ = '(_date + (edate - sdate)) AS _date'
+ $_ = $add_freq->('_date');
}
}
my $select_fields = join(',', @fields);
my $dbh = dbh;
my $sql =
+ # Subquery here because we need to DISTINCT the whole set, select the
+ # latest charge per pkgnum, and _then_ check edate < $eperiod
+ # and edate < expire.
"INSERT INTO v_cust_bill_pkg ($insert_fields)
- SELECT $select_fields FROM v_cust_bill_pkg
- WHERE edate >= $speriod AND edate < $eperiod
+ SELECT $select_fields FROM (
+ SELECT DISTINCT ON (pkgnum) * FROM v_cust_bill_pkg
+ WHERE edate >= $speriod
AND recur > 0
- AND (expire IS NULL OR expire > edate)";
- warn "[extend_projection] $sql\n" if $DEBUG;
- my $rows = $dbh->do($sql) or die $dbh->errstr;
- warn "[extend_projection] $rows rows\n" if $DEBUG;
+ AND freq IS NOT NULL
+ AND freq != '0'
+ ORDER BY pkgnum, edate DESC
+ ) AS v1
+ WHERE edate < $eperiod AND (edate < expire OR expire IS NULL)";
+ my $rows;
+ do {
+ warn "[extend_projection] $sql\n" if $DEBUG;
+ $rows = $dbh->do($sql) or die $dbh->errstr;
+ warn "[extend_projection] $rows rows\n" if $DEBUG;
+ } until $rows == 0;
}
}