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