summaryrefslogtreecommitdiff
path: root/FS/FS/Report/Table/Monthly.pm
blob: b8e52ae637e6c9783dffcf2d175e2892bb0dc110 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
package FS::Report::Table::Monthly;

use strict;
use vars qw( @ISA );
use FS::UID qw(dbh);
use FS::Report::Table;
use Time::Local qw( timelocal );

@ISA = qw( FS::Report::Table );

=head1 NAME

FS::Report::Table::Monthly - Tables of report data, indexed monthly

=head1 SYNOPSIS

  use FS::Report::Table::Monthly;

  my $report = new FS::Report::Table::Monthly (
    'items' => [ 'invoiced', 'netsales', 'credits', 'receipts', ],
    'start_month' => 4,
    'start_year'  => 2000,
    'end_month'   => 4,
    'end_year'    => 2020,
    #opt
    'agentnum'    => 54
    'refnum'      => 54
    'cust_classnum' => [ 1,2,4 ],
    'params'      => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
    'remove_empty' => 1, #collapse empty rows, default 0
    'item_labels' => [ ], #useful with remove_empty
  );

  my $data = $report->data;

=head1 PARAMETERS

=head2 TIME PERIOD

C<start_month>, C<start_year>, C<end_month>, and C<end_year> specify the date
range to be included in the report.  The start and end months are included.
Each month's values are summed from midnight on the first of the month to 
23:59:59 on the last day of the month.

=head2 REPORT ITEMS

=over 4

=item items: An arrayref of observables to calculate for each month.  See 
L<FS::Report::Table> for a list of observables and their parameters.

=item params: An arrayref, parallel to C<items>, of arrayrefs of parameters
(in paired name/value form) to be passed to the observables.

=item cross_params: Cross-product parameters.  This must be an arrayref of 
arrayrefs of parameters (paired name/value form).  This creates an additional 
"axis" (orthogonal to the time and C<items> axes) in which the item is 
calculated once with each set of parameters in C<cross_params>.  These 
parameters are merged with those in C<params>.  Instead of being nested two
levels, C<data> will be nested three levels, with the third level 
corresponding to this arrayref.

=back

=head2 FILTERING

=over 4

=item agentnum: Limit to customers with this agent.

=item refnum: Limit to customers with this advertising source.

=item cust_classnum: Limit to customers with this classnum; can be an 
arrayref.

=item remove_empty: Set this to a true value to hide rows that contain 
only zeroes.  The C<indices> array in the returned data will list the item
indices that are actually present in the output so that you know what they
are.  Ignored if C<cross_params> is in effect.

=back

=head2 PASS-THROUGH

C<item_labels>, C<colors>, and C<links> may be specified as arrayrefs
parallel to C<items>.  Those values will be returned in C<data>, with any
hidden rows (due to C<remove_empty>) filtered out, which is the only 
reason to do this.  Now that we have C<indices> it's probably better to 
use that.

=head1 RETURNED DATA

The C<data> method runs the report and returns a hashref of the following:

=over 4

=item label

Month labels, in MM/YYYY format.

=item speriod, eperiod

Absolute start and end times of each month, in unix time format.

=item items

The values passed in as C<items>, with any suppressed rows deleted.

=item indices

The indices of items in the input C<items> list that appear in the result
set.  Useful for figuring out what they are when C<remove_empty> has deleted 
some items.

=item item_labels, colors, links - see PASS-THROUGH above

=item data

The actual results.  An arrayref corresponding to C<label> (the time axis),
containing arrayrefs corresponding to C<items>, containing either numbers
or, if C<cross_params> is given, arrayrefs corresponding to C<cross_params>.

=back

=cut

sub data {
  local $FS::UID::AutoCommit = 0;
  my $self = shift;

  my $smonth  = $self->{'start_month'};
  my $syear   = $self->{'start_year'};
  my $emonth  = $self->{'end_month'};
  my $eyear   = $self->{'end_year'};
  # whether to extrapolate into the future
  my $projecting = $self->{'projection'};

  # sanity checks
  if ( $eyear < $syear or
      ($eyear == $syear and $emonth < $smonth) ) {
    return { error => 'Start month must be before end month' };
  }

  my $agentnum = $self->{'agentnum'};
  my $refnum = $self->{'refnum'};
  my $cust_classnum = $self->{'cust_classnum'} || [];
  $cust_classnum = [ $cust_classnum ] if !ref($cust_classnum);

  if ( $projecting ) {

    $self->init_projection;

    my $thismonth = $smonth;
    my $thisyear  = $syear;
    while ( $thisyear < $eyear || 
      ( $thisyear == $eyear and $thismonth <= $emonth )
    ) {
      my $speriod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
      $thismonth++;
      if ( $thismonth == 13 ) { $thisyear++; $thismonth = 1; }
      my $eperiod = timelocal(0,0,0,1,$thismonth-1,$thisyear);

      $self->extend_projection($speriod, $eperiod);
    }
  }

  my %data;

  my $max_year  = $eyear;
  my $max_month = $emonth;

  while ( $syear < $max_year
     || ( $syear == $max_year && $smonth < $max_month+1 ) ) {

    push @{$data{label}}, "$smonth/$syear"; # sprintf?

    my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
    push @{$data{speriod}}, $speriod;
    if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
    my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear);
    push @{$data{eperiod}}, $eperiod;

    my $col = 0;
    my @items = @{$self->{'items'}};
    my $i;

    for ( $i = 0; $i < scalar(@items); $i++ ) {
      my $item = $items[$i];
      my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
      push @param, 'project', $projecting;
      push @param, 'refnum' => $refnum if $refnum;
      push @param, 'cust_classnum' => $cust_classnum if @$cust_classnum;

      if ( $self->{'cross_params'} ) {
        my @xdata;
        foreach my $xparam (@{ $self->{'cross_params'} }) {
          # @$xparam is a list of additional params to merge into the list
          my $value = $self->$item($speriod, $eperiod, $agentnum,
                        @param, 
                        @$xparam);
          push @xdata, $value;
        }
        push @{$data{data}->[$col++]}, \@xdata;
      } else {
        my $value = $self->$item($speriod, $eperiod, $agentnum, @param);
        push @{$data{data}->[$col++]}, $value;
      }
    }
  }

  #these need to get generalized, sheesh
  $data{'items'}       = $self->{'items'};
  $data{'item_labels'} = $self->{'item_labels'} || $self->{'items'};
  $data{'colors'}      = $self->{'colors'};
  $data{'links'}       = $self->{'links'} || [];

  if ( !$self->{'cross_params'} and $self->{'remove_empty'} ) {

    my $col = 0;
    #these need to get generalized, sheesh
    #(though we now return a list of item indices that are present in the 
    #output, so the front-end code could do this)
    my @newitems = ();
    my @newlabels = ();
    my @newdata = ();
    my @newcolors = ();
    my @newlinks = ();
    my @indices = ();
    foreach my $item ( @{$self->{'items'}} ) {

      if ( grep { $_ != 0 } @{$data{'data'}->[$col]} ) {
        push @newitems,  $data{'items'}->[$col];
        push @newlabels, $data{'item_labels'}->[$col];
        push @newdata,   $data{'data'}->[$col];
        push @newcolors, $data{'colors'}->[$col];
        push @newlinks,  $data{'links'}->[$col];
        push @indices,   $col;
      }

      $col++;
    }

    $data{'items'}       = \@newitems;
    $data{'item_labels'} = \@newlabels;
    $data{'data'}        = \@newdata;
    $data{'colors'}      = \@newcolors;
    $data{'links'}       = \@newlinks;
    $data{'indices'}     = \@indices;

  }
  # clean up after ourselves
  #dbh->rollback;
  # leave in until development is finished, for diagnostics
  dbh->commit;

  \%data;
}

=back

=head1 BUGS

=head1 SEE ALSO

=cut

1;