add package churn report/graph
[freeside.git] / FS / FS / Report / Table / Monthly.pm
1 package FS::Report::Table::Monthly;
2
3 use strict;
4 use vars qw( @ISA $expenses_kludge );
5 use Time::Local;
6 use FS::UID qw( dbh );
7 use FS::Report::Table;
8 use FS::CurrentUser;
9
10 @ISA = qw( FS::Report::Table );
11
12 $expenses_kludge = 0;
13
14 =head1 NAME
15
16 FS::Report::Table::Monthly - Tables of report data, indexed monthly
17
18 =head1 SYNOPSIS
19
20   use FS::Report::Table::Monthly;
21
22   my $report = new FS::Report::Table::Monthly (
23     'items' => [ 'invoiced', 'netsales', 'credits', 'receipts', ],
24     'start_month' => 4,
25     'start_year'  => 2000,
26     'end_month'   => 4,
27     'end_year'    => 2020,
28     #opt
29     'agentnum'    => 54
30     'params'      => [ [ 'paramsfor', 'item_one' ], [ 'item', 'two' ] ], # ...
31     'remove_empty' => 1, #collapse empty rows, default 0
32     'item_labels' => [ ], #useful with remove_empty
33   );
34
35   my $data = $report->data;
36
37 =head1 METHODS
38
39 =over 4
40
41 =item data
42
43 Returns a hashref of data (!! describe)
44
45 =cut
46
47 sub data {
48   my $self = shift;
49
50   #use Data::Dumper;
51   #warn Dumper($self);
52
53   my $smonth = $self->{'start_month'};
54   my $syear = $self->{'start_year'};
55   my $emonth = $self->{'end_month'};
56   my $eyear = $self->{'end_year'};
57   my $agentnum = $self->{'agentnum'};
58
59   my %data;
60
61   while ( $syear < $eyear || ( $syear == $eyear && $smonth < $emonth+1 ) ) {
62
63     push @{$data{label}}, "$smonth/$syear";
64
65     my $speriod = timelocal(0,0,0,1,$smonth-1,$syear);
66     push @{$data{speriod}}, $speriod;
67     if ( ++$smonth == 13 ) { $syear++; $smonth=1; }
68     my $eperiod = timelocal(0,0,0,1,$smonth-1,$syear);
69     push @{$data{eperiod}}, $eperiod;
70   
71     my $col = 0;
72     my @row = ();
73     foreach my $item ( @{$self->{'items'}} ) {
74       my @param = $self->{'params'} ? @{ $self->{'params'}[$col] }: ();
75       my $value = $self->$item($speriod, $eperiod, $agentnum, @param);
76       #push @{$data{$item}}, $value;
77       push @{$data{data}->[$col++]}, $value;
78     }
79
80   }
81
82   #these need to get generalized, sheesh
83   $data{'items'}       = $self->{'items'};
84   $data{'item_labels'} = $self->{'item_labels'} || $self->{'items'};
85   $data{'colors'}      = $self->{'colors'};
86   $data{'links'}       = $self->{'links'} || [];
87
88   #use Data::Dumper;
89   #warn Dumper(\%data);
90
91   if ( $self->{'remove_empty'} ) {
92
93     #warn "removing empty rows\n";
94
95     my $col = 0;
96     #these need to get generalized, sheesh
97     my @newitems = ();
98     my @newlabels = ();
99     my @newdata = ();
100     my @newcolors = ();
101     my @newlinks = ();
102     foreach my $item ( @{$self->{'items'}} ) {
103
104       if ( grep { $_ != 0 } @{$data{'data'}->[$col]} ) {
105         push @newitems,  $data{'items'}->[$col];
106         push @newlabels, $data{'item_labels'}->[$col];
107         push @newdata,   $data{'data'}->[$col];
108         push @newcolors, $data{'colors'}->[$col];
109         push @newlinks,  $data{'links'}->[$col];
110       }
111
112       $col++;
113     }
114
115     $data{'items'}       = \@newitems;
116     $data{'item_labels'} = \@newlabels;
117     $data{'data'}        = \@newdata;
118     $data{'colors'}      = \@newcolors;
119     $data{'links'}       = \@newlinks;
120
121   }
122
123   #use Data::Dumper;
124   #warn Dumper(\%data);
125
126   \%data;
127
128 }
129
130 sub invoiced { #invoiced
131   my( $self, $speriod, $eperiod, $agentnum ) = @_;
132
133   $self->scalar_sql("
134     SELECT SUM(charged)
135       FROM cust_bill
136         LEFT JOIN cust_main USING ( custnum )
137       WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
138   );
139   
140 }
141
142 sub netsales { #net sales
143   my( $self, $speriod, $eperiod, $agentnum ) = @_;
144
145   my $credited = $self->scalar_sql("
146     SELECT SUM(cust_credit_bill.amount)
147       FROM cust_credit_bill
148         LEFT JOIN cust_bill USING ( invnum  )
149         LEFT JOIN cust_main USING ( custnum )
150     WHERE ".  $self->in_time_period_and_agent( $speriod,
151                                                $eperiod,
152                                                $agentnum,
153                                                'cust_bill._date'
154                                              )
155   );
156
157   #horrible local kludge
158   my $expenses = !$expenses_kludge ? 0 : $self->scalar_sql("
159     SELECT SUM(cust_bill_pkg.setup)
160       FROM cust_bill_pkg
161         LEFT JOIN cust_bill USING ( invnum  )
162         LEFT JOIN cust_main USING ( custnum )
163         LEFT JOIN cust_pkg  USING ( pkgnum  )
164         LEFT JOIN part_pkg  USING ( pkgpart )
165       WHERE ". $self->in_time_period_and_agent( $speriod,
166                                                 $eperiod,
167                                                 $agentnum,
168                                                 'cust_bill._date'
169                                               ). "
170         AND LOWER(part_pkg.pkg) LIKE 'expense _%'
171   ");
172
173   $self->invoiced($speriod,$eperiod,$agentnum) - $credited - $expenses;
174 }
175
176 #deferred revenue
177
178 sub receipts { #cashflow
179   my( $self, $speriod, $eperiod, $agentnum ) = @_;
180
181   my $refunded = $self->scalar_sql("
182     SELECT SUM(refund)
183       FROM cust_refund
184         LEFT JOIN cust_main USING ( custnum )
185       WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
186   );
187
188   #horrible local kludge that doesn't even really work right
189   my $expenses = !$expenses_kludge ? 0 : $self->scalar_sql("
190     SELECT SUM(cust_bill_pay.amount)
191       FROM cust_bill_pay
192         LEFT JOIN cust_bill USING ( invnum  )
193         LEFT JOIN cust_main USING ( custnum )
194     WHERE ". $self->in_time_period_and_agent( $speriod,
195                                               $eperiod,
196                                               $agentnum,
197                                               'cust_bill_pay._date'
198                                             ). "
199     AND 0 < ( SELECT COUNT(*) from cust_bill_pkg, cust_pkg, part_pkg
200               WHERE cust_bill.invnum = cust_bill_pkg.invnum
201               AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum
202               AND cust_pkg.pkgpart = part_pkg.pkgpart
203               AND LOWER(part_pkg.pkg) LIKE 'expense _%'
204             )
205   ");
206   #    my $expenses_sql2 = "SELECT SUM(cust_bill_pay.amount) FROM cust_bill_pay, cust_bill_pkg, cust_bill, cust_pkg, part_pkg WHERE cust_bill_pay.invnum = cust_bill.invnum AND cust_bill.invnum = cust_bill_pkg.invnum AND cust_bill_pay._date >= $speriod AND cust_bill_pay._date < $eperiod AND cust_pkg.pkgnum = cust_bill_pkg.pkgnum AND cust_pkg.pkgpart = part_pkg.pkgpart AND LOWER(part_pkg.pkg) LIKE 'expense _%'";
207   
208   $self->payments($speriod, $eperiod, $agentnum) - $refunded - $expenses;
209 }
210
211 sub payments {
212   my( $self, $speriod, $eperiod, $agentnum ) = @_;
213   $self->scalar_sql("
214     SELECT SUM(paid)
215       FROM cust_pay
216         LEFT JOIN cust_main USING ( custnum )
217       WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
218   );
219 }
220
221 sub credits {
222   my( $self, $speriod, $eperiod, $agentnum ) = @_;
223   $self->scalar_sql("
224     SELECT SUM(amount)
225       FROM cust_credit
226         LEFT JOIN cust_main USING ( custnum )
227       WHERE ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
228   );
229 }
230
231 #these should be auto-generated or $AUTOLOADed or something
232 sub invoiced_12mo {
233   my( $self, $speriod, $eperiod, $agentnum ) = @_;
234   $speriod = $self->_subtract_11mo($speriod);
235   $self->invoiced($speriod, $eperiod, $agentnum);
236 }
237
238 sub netsales_12mo {
239   my( $self, $speriod, $eperiod, $agentnum ) = @_;
240   $speriod = $self->_subtract_11mo($speriod);
241   $self->netsales($speriod, $eperiod, $agentnum);
242 }
243
244 sub receipts_12mo {
245   my( $self, $speriod, $eperiod, $agentnum ) = @_;
246   $speriod = $self->_subtract_11mo($speriod);
247   $self->receipts($speriod, $eperiod, $agentnum);
248 }
249
250 sub payments_12mo {
251   my( $self, $speriod, $eperiod, $agentnum ) = @_;
252   $speriod = $self->_subtract_11mo($speriod);
253   $self->payments($speriod, $eperiod, $agentnum);
254 }
255
256 sub credits_12mo {
257   my( $self, $speriod, $eperiod, $agentnum ) = @_;
258   $speriod = $self->_subtract_11mo($speriod);
259   $self->credits($speriod, $eperiod, $agentnum);
260 }
261
262 #not being too bad with the false laziness
263 use Time::Local qw(timelocal);
264 sub _subtract_11mo {
265   my($self, $time) = @_;
266   my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($time) )[0,1,2,3,4,5];
267   $mon -= 11;
268   if ( $mon < 0 ) { $mon+=12; $year--; }
269   timelocal($sec,$min,$hour,$mday,$mon,$year);
270 }
271
272 sub cust_bill_pkg {
273   my( $self, $speriod, $eperiod, $agentnum, %opt ) = @_;
274
275   my $where = '';
276   if ( $opt{'classnum'} =~ /^(\d+)$/ ) {
277     if ( $1 == 0 ) {
278       $where = "classnum IS NULL";
279     } else {
280       $where = "classnum = $1";
281     }
282   }
283
284   $agentnum ||= $opt{'agentnum'};
285
286   $self->scalar_sql("
287     SELECT SUM(cust_bill_pkg.setup + cust_bill_pkg.recur)
288       FROM cust_bill_pkg
289         LEFT JOIN cust_bill USING ( invnum )
290         LEFT JOIN cust_main USING ( custnum )
291         LEFT JOIN cust_pkg USING ( pkgnum )
292         LEFT JOIN part_pkg USING ( pkgpart )
293       WHERE pkgnum != 0
294         AND $where
295         AND ". $self->in_time_period_and_agent($speriod, $eperiod, $agentnum)
296   );
297   
298 }
299
300 sub setup_pkg  { shift->pkg_field( @_, 'setup' ); }
301 sub susp_pkg   { shift->pkg_field( @_, 'susp'  ); }
302 sub cancel_pkg { shift->pkg_field( @_, 'cancel'); }
303  
304 sub pkg_field {
305   my( $self, $speriod, $eperiod, $agentnum, $field ) = @_;
306   $self->scalar_sql("
307     SELECT COUNT(*) FROM cust_pkg
308         LEFT JOIN cust_main USING ( custnum )
309       WHERE ". $self->in_time_period_and_agent( $speriod,
310                                                 $eperiod,
311                                                 $agentnum,
312                                                 "cust_pkg.$field",
313                                               )
314   );
315
316 }
317
318 #this is going to be harder..
319 #sub unsusp_pkg {
320 #  my( $self, $speriod, $eperiod, $agentnum ) = @_;
321 #  $self->scalar_sql("
322 #    SELECT COUNT(*) FROM h_cust_pkg
323 #      WHERE 
324 #
325 #}
326
327 sub in_time_period_and_agent {
328   my( $self, $speriod, $eperiod, $agentnum ) = splice(@_, 0, 4);
329   my $col = @_ ? shift() : '_date';
330
331   my $sql = "$col >= $speriod AND $col < $eperiod";
332
333   #agent selection
334   $sql .= " AND agentnum = $agentnum"
335     if $agentnum;
336
337   #agent virtualization
338   $sql .= ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
339
340   $sql;
341 }
342
343 sub scalar_sql {
344   my( $self, $sql ) = ( shift, shift );
345   my $sth = dbh->prepare($sql) or die dbh->errstr;
346   $sth->execute
347     or die "Unexpected error executing statement $sql: ". $sth->errstr;
348   $sth->fetchrow_arrayref->[0] || 0;
349 }
350
351 =back
352
353 =head1 BUGS
354
355 Documentation.
356
357 =head1 SEE ALSO
358
359 =cut
360
361 1;
362