fix A/R report
[freeside.git] / httemplate / graph / cust_bill_pkg.cgi
1 <% include('elements/monthly.html',
2                 'title'        => $title,
3                 'graph_type'   => $graph_type,
4                 'items'        => \@items,
5                 'params'       => \@params,
6                 'labels'       => \@labels,
7                 'graph_labels' => \@labels,
8                 'colors'       => \@colors,
9                 'links'        => \@links,
10                 'no_graph'     => \@no_graph,
11                 'remove_empty' => 1,
12                 'bottom_total' => $show_total,
13                 'nototal'      => !$show_total,
14                 'bottom_link'  => $bottom_link,
15                 'agentnum'     => $agentnum,
16                 'cust_classnum'=> \@cust_classnums,
17              )
18 %>
19 <%init>
20
21 die "access denied"
22   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
23
24 my $link = "${p}search/cust_bill_pkg.cgi?nottax=1";
25 my $bottom_link = "$link;";
26
27 my $use_usage = $cgi->param('use_usage') || 0;
28 my $use_setup = $cgi->param('use_setup') || 0;
29 my $use_discount = $cgi->param('use_discount') || 2;
30 my $use_taxes = $cgi->param('use_taxes') || 0;
31
32 my $use_override         = $cgi->param('use_override')         ? 1 : 0;
33 my $average_per_cust_pkg = $cgi->param('average_per_cust_pkg') ? 1 : 0;
34 my $distribute           = $cgi->param('distribute')           ? 1 : 0;
35
36 my $show_total = 1;
37 my $graph_type = 'Mountain';
38
39 if ( $average_per_cust_pkg ) {
40   # then the rows are not additive
41   $show_total = 0;
42   $graph_type = 'LinesPoints';
43 }
44
45 my %charge_labels = (
46   'SRU'=> 'setup + recurring',
47   'SR' => 'setup + recurring',
48   'RU' => 'recurring',
49   'S'  => 'setup',
50   'R'  => 'recurring',
51   'U'  => 'usage',
52   'D'  => 'discount',
53   'T'  => 'taxes',
54 );
55
56 #XXX or virtual
57 my( $agentnum, $sel_agent, $all_agent ) = ('', '', '');
58 if ( $cgi->param('agentnum') eq 'all' ) {
59   $agentnum = 0;
60   $all_agent = 'ALL';
61 }
62 elsif ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
63   $agentnum = $1;
64   $bottom_link .= "agentnum=$agentnum;";
65   $sel_agent = qsearchs('agent', { 'agentnum' => $agentnum } );
66   die "agentnum $agentnum not found!" unless $sel_agent;
67 }
68 my $title = $sel_agent ? $sel_agent->agent.' ' : '';
69
70 my( $refnum, $sel_part_referral, $all_part_referral ) = ('', '', '');
71 if ( $cgi->param('refnum') eq 'all' ) {
72   $refnum = 0;
73   $all_part_referral = 'ALL';
74 }
75 elsif ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
76   $refnum = $1;
77   $bottom_link .= "refnum=$refnum;";
78   $sel_part_referral = qsearchs('part_referral', { 'refnum' => $refnum } );
79   die "part_referral $refnum not found!" unless $sel_part_referral;
80 }
81 $title .= $sel_part_referral->referral.' '
82   if $sel_part_referral;
83
84 $title .= 'Sales Report (Gross)';
85 $title .= ', average per customer package'  if $average_per_cust_pkg;
86
87 my @cust_classnums = grep /^\d+$/, $cgi->param('cust_classnum');
88 $bottom_link .= "cust_classnum=$_;" foreach @cust_classnums;
89
90 #classnum (here)
91 # not specified: no longer happens (unless you de-select all classes)
92 # 0: empty class
93 # N: classnum
94 #classnum (link)
95 # not specified: all classes
96 # 0: empty class
97 # N: classnum
98
99 #started out as false lazinessish w/FS::cust_pkg::search_sql (previously search/cust_pkg.cgi), but not much left the sane now after #24776
100
101 my ($class_table, $name_col, $value_col, $class_param);
102 my $all_report_options;
103
104 if ( $cgi->param('class_mode') eq 'report' ) {
105   $class_param = 'report_optionnum'; # CGI param name, also used in the report engine
106   $class_table = 'part_pkg_report_option'; # table containing classes
107   $name_col = 'name'; # the column of that table containing the label
108   $value_col = 'num'; # the column containing the class number
109   # in 'exact' mode we want to run the query in ALL mode.
110   # in 'breakdown' mode want to run the query in ALL mode but using the 
111   # power set of the classes selected.
112   $all_report_options = 1
113     unless $cgi->param('class_agg_break') eq 'aggregate';
114 } else { # class_mode eq 'pkg'
115   $class_param = 'classnum';
116   $class_table = 'pkg_class';
117   $name_col = 'classname';
118   $value_col = 'classnum';
119 }
120
121 my @classnums = sort {$a <=> $b} grep /^\d+$/, $cgi->param($class_param);
122 my @classnames = map { if ( $_ ) {
123                          my $class = qsearchs($class_table, {$value_col=>$_} );
124                          $class->$name_col;
125                        } else {
126                          '(empty class)';
127                        }
128                      }
129                    @classnums;
130 my @not_classnums;
131
132 $bottom_link .= "$class_param=$_;" foreach @classnums;
133
134 if ( $cgi->param('class_agg_break') eq 'aggregate' or
135      $cgi->param('class_agg_break') eq 'exact' ) {
136
137   $title .= ' '. join(', ', @classnames)
138     unless scalar(@classnames) > scalar(qsearch($class_table,{'disabled'=>''}));
139                                  #not efficient for lots of package classes
140
141 } elsif ( $cgi->param('class_agg_break') eq 'breakdown' ) {
142
143   if ( $cgi->param('class_mode') eq 'report' ) {
144     # The new way:
145     # Actually break down all subsets of the (selected) report classes.
146     my @subsets = FS::part_pkg_report_option->subsets(@classnums);
147     my @classnum_space = @classnums;
148     @classnums = @classnames = ();
149     while(@subsets) {
150       my $these = shift @subsets;
151       # applied topology!
152       my $not_these = [ @classnum_space ];
153       my $i = 0;
154       foreach (@$these) {
155         $i++ until $not_these->[$i] == $_;
156         splice(@$not_these, $i, 1);
157       }
158       push @classnums, $these;
159       push @not_classnums, $not_these;
160       push @classnames, shift @subsets;
161     } #while subsets
162   }
163   # else it's 'pkg', i.e. part_pkg.classnum, which is singular on pkgpart
164   # and much simpler
165
166 } else {
167   die "guru meditation #434";
168 }
169
170 #eslaf
171
172 my @items  = ();
173 my @params = ();
174 my @labels = ();
175 my @colors = ();
176 my @links  = ();
177 my @no_graph;
178
179 my @components = ( 'SRU' );
180 # split/omit components as appropriate
181 if ( $use_setup == 1 ) {
182   @components = ( 'S', 'RU' );
183 }
184 elsif ( $use_setup == 2 ) {
185   @components = ( 'RU' );
186 }
187 if ( $use_usage == 1 ) {
188   $components[-1] =~ s/U//; push @components, 'U';
189 }
190 elsif ( $use_usage == 2 ) {
191   $components[-1] =~ s/U//;
192 }
193
194 if ( $use_discount == 1 ) {
195   push @components, 'D';
196 } # else leave discounts off entirely; never combine them with setup/recur
197
198 # could in theory combine with setup/recur/usage,
199 # but would require reverse engineering the tax calculation
200 if ( $use_taxes == 1 ) {
201   push @components, 'T';
202
203
204 # Categorization of line items goes
205 # Agent -> Referral -> Package class -> Component (setup/recur/usage/discount/taxes)
206 # If per-agent totals are enabled, they go under the Agent level.
207 # There aren't any other kinds of subtotals.
208
209 my $anum = 0;
210 foreach my $agent ( $all_agent || $sel_agent || $FS::CurrentUser::CurrentUser->agents ) {
211
212   my @agent_colors = map { my $col = $cgi->param("agent$anum-color$_");
213                            $col =~ s/^#//;
214                            $col;
215                          }
216                        (0 .. 5);
217   my @colorbuf = ();
218
219   ### fixup the color handling for package classes...
220   ### and usage
221
222   foreach my $part_referral (
223     $all_part_referral ||
224     $sel_part_referral ||
225     qsearch('part_referral', { 'disabled' => '' } ) 
226   ) {
227
228     my @base_params = (
229                         'use_override'         => $use_override,
230                         'average_per_cust_pkg' => $average_per_cust_pkg,
231                         'distribute'           => $distribute,
232                       );
233
234     if ( $cgi->param('class_agg_break') eq 'aggregate' or
235          $cgi->param('class_agg_break') eq 'exact' ) {
236       # the only difference between 'aggregate' and 'exact' is whether
237       # we pass the 'all_report_options' flag.
238
239       foreach my $component ( @components ) {
240
241         push @items, 'cust_bill_pkg';
242
243         push @labels,
244           ( $all_agent || $sel_agent ? '' : $agent->agent.' ' ).
245           ( $all_part_referral || $sel_part_referral ? '' : $part_referral->referral.' ' ).
246           $charge_labels{$component};
247
248         my $row_agentnum = $all_agent || $agent->agentnum;
249         my $row_refnum = $all_part_referral || $part_referral->refnum;
250         my @row_params = (
251                         @base_params,
252                         $class_param => \@classnums,
253                         ($all_agent ? () : ('agentnum' => $row_agentnum) ),
254                         ($all_part_referral ? () : ('refnum' => $row_refnum) ),
255                         'charges'               => $component,
256         );
257
258         my $row_link = "$link;".
259                        "charges=$component;".
260                        "distribute=$distribute;";
261
262         if ( $component eq 'D' ) {
263           # discounts ignore 'charges' and 'distribute'
264           $row_link = "${p}search/cust_bill_pkg_discount.html?";
265         } elsif ( $component eq 'T' ) {
266           $row_link = "${p}search/cust_bill_pkg.cgi?istax=1;";
267         }
268
269         $row_link .=  ($all_agent ? '' : "agentnum=$row_agentnum;").
270                       ($all_part_referral ? '' : "refnum=$row_refnum;").
271                       (join('',map {"cust_classnum=$_;"} @cust_classnums)).
272                       "use_override=$use_override;";
273         $row_link .= "$class_param=$_;" foreach @classnums;
274         if ( $all_report_options ) {
275           push @row_params, 'all_report_options', 1;
276           $row_link .= 'all_report_options=1';
277         }
278         push @params, \@row_params;
279         push @links, $row_link;
280
281         @colorbuf = @agent_colors unless @colorbuf;
282         push @colors, shift @colorbuf;
283         push @no_graph, 0;
284
285       } #foreach $component
286
287     } elsif ( $cgi->param('class_agg_break') eq 'breakdown' ) {
288
289       for (my $i = 0; $i < scalar @classnums; $i++) {
290         my $row_classnum = $classnums[$i];
291         my $row_classname = $classnames[$i];
292         my $not_row_classnum = '';
293         if ( $class_param eq 'report_optionnum' ) {
294           # if we're working with report options, @classnums here contains 
295           # arrays of multiple classnums
296           $row_classnum = join(',', @$row_classnum);
297           $row_classname = join(', ', @$row_classname);
298           $not_row_classnum = join(',', @{ $not_classnums[$i] });
299         }
300         foreach my $component ( @components ) {
301
302           push @items, 'cust_bill_pkg';
303
304           push @labels,
305             ( $all_agent || $sel_agent ? '' : $agent->agent.' ' ).
306             ( $all_part_referral || $sel_part_referral ? '' : $part_referral->referral.' ' ).
307             $row_classname .  ' ' . $charge_labels{$component};
308
309           my $row_agentnum = $all_agent || $agent->agentnum;
310           my $row_refnum = $all_part_referral || $part_referral->refnum;
311           my @row_params = (
312                           @base_params,
313                           $class_param => $row_classnum,
314                           ($all_agent ? () : ('agentnum' => $row_agentnum) ),
315                           ($all_part_referral ? () : ('refnum' => $row_refnum)),
316                           'charges'              => $component,
317           );
318
319           my $row_link = "$link;".
320                        "charges=$component;".
321                        "distribute=$distribute;";
322
323           if ( $component eq 'D' ) {
324             # discounts ignore 'charges' and 'distribute'
325             $row_link ="${p}search/cust_bill_pkg_discount.html?";
326           } elsif ( $component eq 'T' ) {
327             $row_link = "${p}search/cust_bill_pkg.cgi?istax=1;";
328           }
329
330           $row_link .= ($all_agent ? '' : "agentnum=$row_agentnum;").
331                        ($all_part_referral ? '' : "refnum=$row_refnum;").
332                        (join('',map {"cust_classnum=$_;"} @cust_classnums)).
333                        "$class_param=$row_classnum;".
334                        "use_override=$use_override;";
335
336           if ( $class_param eq 'report_optionnum' ) {
337             push @row_params,
338                           'all_report_options' => 1,
339                           'not_report_optionnum' => $not_row_classnum,
340             ;
341             $row_link .= "all_report_options=1;".
342                          "not_report_optionnum=$not_row_classnum;";
343           }
344           push @params, \@row_params;
345           push @links, $row_link;
346
347           @colorbuf = @agent_colors unless @colorbuf;
348           push @colors, shift @colorbuf;
349           push @no_graph, 0;
350
351         } #foreach $component
352       } #foreach $row_classnum
353
354     } #$cgi->param('class_agg_break')
355
356   } #foreach $part_referral
357
358   if ( $cgi->param('agent_totals') and !$all_agent ) {
359     my $row_agentnum = $agent->agentnum;
360     # Include all components that are anywhere on this report
361     my $component = join('', @components);
362
363     my @row_params = (  'agentnum'              => $row_agentnum,
364                         'cust_classnum'         => \@cust_classnums,
365                         'use_override'          => $use_override,
366                         'average_per_cust_pkg'  => $average_per_cust_pkg,
367                         'distribute'            => $distribute,
368                         'charges'               => $component,
369                      );
370     my $row_link = "$link;".
371                    "agentnum=$row_agentnum;".
372                    "distribute=$distribute;".
373                    "charges=$component;";
374     
375     # package class filters
376     if ( $cgi->param('class_agg_break') eq 'aggregate' ) {
377       push @row_params, $class_param => \@classnums;
378       $row_link .= "$class_param=$_;" foreach @classnums;
379     }
380
381     # refnum filters
382     if ( $sel_part_referral ) {
383       push @row_params, 'refnum' => $sel_part_referral->refnum;
384       $row_link .= "refnum=;".$sel_part_referral->refnum;
385     }
386
387     # customer class filters
388     $row_link .= "cust_classnum=$_;" foreach @cust_classnums;
389
390     push @items, 'cust_bill_pkg';
391     push @labels, mt('[_1] - Subtotal', $agent->agent);
392     push @params, \@row_params;
393     push @links, $row_link;
394     push @colors, '000000'; # better idea?
395     push @no_graph, 1;
396   }
397
398   $anum++;
399
400 } # foreach $agent
401
402 if ( $cgi->param('debug') == 1 ) {
403   $FS::Report::Table::DEBUG = 1;
404 }
405 </%init>