filter by customer class on all financial reports, #20573
[freeside.git] / FS / FS / Report / Table / Monthly.pm
1 package FS::Report::Table::Monthly;
2
3 use strict;
4 use vars qw( @ISA );
5 use FS::UID qw(dbh);
6 use FS::Report::Table;
7 use Time::Local qw( timelocal );
8
9 @ISA = qw( FS::Report::Table );
10
11 =head1 NAME
12
13 FS::Report::Table::Monthly - Tables of report data, indexed monthly
14
15 =head1 SYNOPSIS
16
17   use FS::Report::Table::Monthly;
18
19   my $report = new FS::Report::Table::Monthly (
20     'items' => [ 'invoiced', 'netsales', 'credits', 'receipts', ],
21     'start_month' => 4,
22     'start_year'  => 2000,
23     'end_month'   => 4,
24     'end_year'    => 2020,
25     #opt
26     'agentnum'    => 54
27     'refnum'      => 54
28     'cust_classnum' => [ 1,2,4 ],
29     'params'      => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
30     'remove_empty' => 1, #collapse empty rows, default 0
31     'item_labels' => [ ], #useful with remove_empty
32   );
33
34   my $data = $report->data;
35
36 =head1 PARAMETERS
37
38 =head2 TIME PERIOD
39
40 C<start_month>, C<start_year>, C<end_month>, and C<end_year> specify the date
41 range to be included in the report.  The start and end months are included.
42 Each month's values are summed from midnight on the first of the month to 
43 23:59:59 on the last day of the month.
44
45 =head2 REPORT ITEMS
46
47 =over 4
48
49 =item items: An arrayref of observables to calculate for each month.  See 
50 L<FS::Report::Table> for a list of observables and their parameters.
51
52 =item params: An arrayref, parallel to C<items>, of arrayrefs of parameters
53 (in paired name/value form) to be passed to the observables.
54
55 =item cross_params: Cross-product parameters.  This must be an arrayref of 
56 arrayrefs of parameters (paired name/value form).  This creates an additional 
57 "axis" (orthogonal to the time and C<items> axes) in which the item is 
58 calculated once with each set of parameters in C<cross_params>.  These 
59 parameters are merged with those in C<params>.  Instead of being nested two
60 levels, C<data> will be nested three levels, with the third level 
61 corresponding to this arrayref.
62
63 =back
64
65 =head2 FILTERING
66
67 =over 4
68
69 =item agentnum: Limit to customers with this agent.
70
71 =item refnum: Limit to customers with this advertising source.
72
73 =item cust_classnum: Limit to customers with this classnum; can be an 
74 arrayref.
75
76 =item remove_empty: Set this to a true value to hide rows that contain 
77 only zeroes.  The C<indices> array in the returned data will list the item
78 indices that are actually present in the output so that you know what they
79 are.  Ignored if C<cross_params> is in effect.
80
81 =back
82
83 =head2 PASS-THROUGH
84
85 C<item_labels>, C<colors>, and C<links> may be specified as arrayrefs
86 parallel to C<items>.  Those values will be returned in C<data>, with any
87 hidden rows (due to C<remove_empty>) filtered out, which is the only 
88 reason to do this.  Now that we have C<indices> it's probably better to 
89 use that.
90
91 =head1 RETURNED DATA
92
93 The C<data> method runs the report and returns a hashref of the following:
94
95 =over 4
96
97 =item label
98
99 Month labels, in MM/YYYY format.
100
101 =item speriod, eperiod
102
103 Absolute start and end times of each month, in unix time format.
104
105 =item items
106
107 The values passed in as C<items>, with any suppressed rows deleted.
108
109 =item indices
110
111 The indices of items in the input C<items> list that appear in the result
112 set.  Useful for figuring out what they are when C<remove_empty> has deleted 
113 some items.
114
115 =item item_labels, colors, links - see PASS-THROUGH above
116
117 =item data
118
119 The actual results.  An arrayref corresponding to C<label> (the time axis),
120 containing arrayrefs corresponding to C<items>, containing either numbers
121 or, if C<cross_params> is given, arrayrefs corresponding to C<cross_params>.
122
123 =back
124
125 =cut
126
127 sub data {
128   local $FS::UID::AutoCommit = 0;
129   my $self = shift;
130
131   my $smonth  = $self->{'start_month'};
132   my $syear   = $self->{'start_year'};
133   my $emonth  = $self->{'end_month'};
134   my $eyear   = $self->{'end_year'};
135   # whether to extrapolate into the future
136   my $projecting = $self->{'projection'};
137
138   # sanity checks
139   if ( $eyear < $syear or
140       ($eyear == $syear and $emonth < $smonth) ) {
141     return { error => 'Start month must be before end month' };
142   }
143
144   my $agentnum = $self->{'agentnum'};
145   my $refnum = $self->{'refnum'};
146   my $cust_classnum = $self->{'cust_classnum'} || [];
147   $cust_classnum = [ $cust_classnum ] if !ref($cust_classnum);
148
149   if ( $projecting ) {
150
151     $self->init_projection;
152
153     my $thismonth = $smonth;
154     my $thisyear  = $syear;
155     while ( $thisyear < $eyear || 
156       ( $thisyear == $eyear and $thismonth <= $emonth )
157     ) {
158       my $speriod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
159       $thismonth++;
160       if ( $thismonth == 13 ) { $thisyear++; $thismonth = 1; }
161       my $eperiod = timelocal(0,0,0,1,$thismonth-1,$thisyear);
162
163       $self->extend_projection($speriod, $eperiod);
164     }
165   }
166
167   my %data;
168
169   my $max_year  = $eyear;
170   my $max_month = $emonth;
171
172   while ( $syear < $max_year
173      || ( $syear == $max_year && $smonth < $max_month+1 ) ) {
174
175     push @{$data{label}}, "$smonth/$syear"; # sprintf?
176
177     my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
178     push @{$data{speriod}}, $speriod;
179     if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
180     my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear);
181     push @{$data{eperiod}}, $eperiod;
182
183     my $col = 0;
184     my @items = @{$self->{'items'}};
185     my $i;
186
187     for ( $i = 0; $i < scalar(@items); $i++ ) {
188       my $item = $items[$i];
189       my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
190       push @param, 'project', $projecting;
191       push @param, 'refnum' => $refnum if $refnum;
192       push @param, 'cust_classnum' => $cust_classnum if @$cust_classnum;
193
194       if ( $self->{'cross_params'} ) {
195         my @xdata;
196         foreach my $xparam (@{ $self->{'cross_params'} }) {
197           # @$xparam is a list of additional params to merge into the list
198           my $value = $self->$item($speriod, $eperiod, $agentnum,
199                         @param, 
200                         @$xparam);
201           push @xdata, $value;
202         }
203         push @{$data{data}->[$col++]}, \@xdata;
204       } else {
205         my $value = $self->$item($speriod, $eperiod, $agentnum, @param);
206         push @{$data{data}->[$col++]}, $value;
207       }
208     }
209   }
210
211   #these need to get generalized, sheesh
212   $data{'items'}       = $self->{'items'};
213   $data{'item_labels'} = $self->{'item_labels'} || $self->{'items'};
214   $data{'colors'}      = $self->{'colors'};
215   $data{'links'}       = $self->{'links'} || [];
216
217   if ( !$self->{'cross_params'} and $self->{'remove_empty'} ) {
218
219     my $col = 0;
220     #these need to get generalized, sheesh
221     #(though we now return a list of item indices that are present in the 
222     #output, so the front-end code could do this)
223     my @newitems = ();
224     my @newlabels = ();
225     my @newdata = ();
226     my @newcolors = ();
227     my @newlinks = ();
228     my @indices = ();
229     foreach my $item ( @{$self->{'items'}} ) {
230
231       if ( grep { $_ != 0 } @{$data{'data'}->[$col]} ) {
232         push @newitems,  $data{'items'}->[$col];
233         push @newlabels, $data{'item_labels'}->[$col];
234         push @newdata,   $data{'data'}->[$col];
235         push @newcolors, $data{'colors'}->[$col];
236         push @newlinks,  $data{'links'}->[$col];
237         push @indices,   $col;
238       }
239
240       $col++;
241     }
242
243     $data{'items'}       = \@newitems;
244     $data{'item_labels'} = \@newlabels;
245     $data{'data'}        = \@newdata;
246     $data{'colors'}      = \@newcolors;
247     $data{'links'}       = \@newlinks;
248     $data{'indices'}     = \@indices;
249
250   }
251   # clean up after ourselves
252   #dbh->rollback;
253   # leave in until development is finished, for diagnostics
254   dbh->commit;
255
256   \%data;
257 }
258
259 =back
260
261 =head1 BUGS
262
263 =head1 SEE ALSO
264
265 =cut
266
267 1;
268