1 package FS::Report::Table::Monthly;
7 use Time::Local qw( timelocal );
9 @ISA = qw( FS::Report::Table );
13 FS::Report::Table::Monthly - Tables of report data, indexed monthly
17 use FS::Report::Table::Monthly;
19 my $report = new FS::Report::Table::Monthly (
20 'items' => [ 'invoiced', 'netsales', 'credits', 'receipts', ],
28 'params' => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
29 'remove_empty' => 1, #collapse empty rows, default 0
30 'item_labels' => [ ], #useful with remove_empty
33 my $data = $report->data;
39 C<start_month>, C<start_year>, C<end_month>, and C<end_year> specify the date
40 range to be included in the report. The start and end months are included.
41 Each month's values are summed from midnight on the first of the month to
42 23:59:59 on the last day of the month.
48 =item items: An arrayref of observables to calculate for each month. See
49 L<FS::Report::Table> for a list of observables and their parameters.
51 =item params: An arrayref, parallel to C<items>, of arrayrefs of parameters
52 (in paired name/value form) to be passed to the observables.
54 =item cross_params: Cross-product parameters. This must be an arrayref of
55 arrayrefs of parameters (paired name/value form). This creates an additional
56 "axis" (orthogonal to the time and C<items> axes) in which the item is
57 calculated once with each set of parameters in C<cross_params>. These
58 parameters are merged with those in C<params>. Instead of being nested two
59 levels, C<data> will be nested three levels, with the third level
60 corresponding to this arrayref.
68 =item agentnum: Limit to customers with this agent.
70 =item refnum: Limit to customers with this advertising source.
72 =item remove_empty: Set this to a true value to hide rows that contain
73 only zeroes. The C<indices> array in the returned data will list the item
74 indices that are actually present in the output so that you know what they
75 are. Ignored if C<cross_params> is in effect.
81 C<item_labels>, C<colors>, and C<links> may be specified as arrayrefs
82 parallel to C<items>. Those values will be returned in C<data>, with any
83 hidden rows (due to C<remove_empty>) filtered out, which is the only
84 reason to do this. Now that we have C<indices> it's probably better to
89 The C<data> method runs the report and returns a hashref of the following:
95 Month labels, in MM/YYYY format.
97 =item speriod, eperiod
99 Absolute start and end times of each month, in unix time format.
103 The values passed in as C<items>, with any suppressed rows deleted.
107 The indices of items in the input C<items> list that appear in the result
108 set. Useful for figuring out what they are when C<remove_empty> has deleted
111 =item item_labels, colors, links - see PASS-THROUGH above
115 The actual results. An arrayref corresponding to C<label> (the time axis),
116 containing arrayrefs corresponding to C<items>, containing either numbers
117 or, if C<cross_params> is given, arrayrefs corresponding to C<cross_params>.
124 local $FS::UID::AutoCommit = 0;
127 my $smonth = $self->{'start_month'};
128 my $syear = $self->{'start_year'};
129 my $emonth = $self->{'end_month'};
130 my $eyear = $self->{'end_year'};
131 # whether to extrapolate into the future
132 my $projecting = $self->{'projection'};
135 if ( $eyear < $syear or
136 ($eyear == $syear and $emonth < $smonth) ) {
137 return { error => 'Start month must be before end month' };
140 my $agentnum = $self->{'agentnum'};
141 my $refnum = $self->{'refnum'};
145 $self->init_projection;
147 my $thismonth = $smonth;
148 my $thisyear = $syear;
149 while ( $thisyear < $eyear ||
150 ( $thisyear == $eyear and $thismonth <= $emonth )
152 my $speriod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
154 if ( $thismonth == 13 ) { $thisyear++; $thismonth = 1; }
155 my $eperiod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
157 $self->extend_projection($speriod, $eperiod);
163 my $max_year = $eyear;
164 my $max_month = $emonth;
166 while ( $syear < $max_year
167 || ( $syear == $max_year && $smonth < $max_month+1 ) ) {
169 push @{$data{label}}, "$smonth/$syear"; # sprintf?
171 my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
172 push @{$data{speriod}}, $speriod;
173 if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
174 my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear);
175 push @{$data{eperiod}}, $eperiod;
178 my @items = @{$self->{'items'}};
181 for ( $i = 0; $i < scalar(@items); $i++ ) {
182 my $item = $items[$i];
183 my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
184 push @param, 'project', $projecting;
185 push @param, 'refnum' => $refnum if $refnum;
187 if ( $self->{'cross_params'} ) {
189 foreach my $xparam (@{ $self->{'cross_params'} }) {
190 # @$xparam is a list of additional params to merge into the list
191 my $value = $self->$item($speriod, $eperiod, $agentnum,
196 push @{$data{data}->[$col++]}, \@xdata;
198 my $value = $self->$item($speriod, $eperiod, $agentnum, @param);
199 push @{$data{data}->[$col++]}, $value;
204 #these need to get generalized, sheesh
205 $data{'items'} = $self->{'items'};
206 $data{'item_labels'} = $self->{'item_labels'} || $self->{'items'};
207 $data{'colors'} = $self->{'colors'};
208 $data{'links'} = $self->{'links'} || [];
210 if ( !$self->{'cross_params'} and $self->{'remove_empty'} ) {
213 #these need to get generalized, sheesh
214 #(though we now return a list of item indices that are present in the
215 #output, so the front-end code could do this)
222 foreach my $item ( @{$self->{'items'}} ) {
224 if ( grep { $_ != 0 } @{$data{'data'}->[$col]} ) {
225 push @newitems, $data{'items'}->[$col];
226 push @newlabels, $data{'item_labels'}->[$col];
227 push @newdata, $data{'data'}->[$col];
228 push @newcolors, $data{'colors'}->[$col];
229 push @newlinks, $data{'links'}->[$col];
236 $data{'items'} = \@newitems;
237 $data{'item_labels'} = \@newlabels;
238 $data{'data'} = \@newdata;
239 $data{'colors'} = \@newcolors;
240 $data{'links'} = \@newlinks;
241 $data{'indices'} = \@indices;
244 # clean up after ourselves
246 # leave in until development is finished, for diagnostics