remove trailing junk from XLSX files, #20337
[freeside.git] / httemplate / search / customer_accounting_summary.html
1 % if ( $cgi->param('_type') =~ /(xls)$/ ) {
2 <%perl>
3   # egregious false laziness w/ search/report_tax-xls.cgi
4   my $format = $FS::CurrentUser::CurrentUser->spreadsheet_format;
5   my $filename = $cgi->url(-relative => 1);
6   $filename =~ s/\.html$//;
7   $filename .= $format->{extension};
8   http_header('Content-Type' => $format->{mime_type});
9   http_header('Content-Disposition' => qq!attachment;filename="$filename"!);
10
11   my $output = '';
12   use IO::String;
13   my $XLS = IO::String->new($output);;
14   my $workbook = $format->{class}->new($XLS)
15     or die "Error opening .xls file: $!";
16
17   my $worksheet = $workbook->add_worksheet('Summary');
18
19   my %format = (
20     header => {
21       size      => 11,
22       bold      => 1,
23       align     => 'center',
24       valign    => 'vcenter',
25       text_wrap => 1,
26     },
27     money => {
28       size      => 11,
29       align     => 'right',
30       valign    => 'bottom',
31       num_format=> 8,
32     },
33     '' => {},
34   );
35   my %default = (
36       font      => 'Calibri',
37       border    => 1,
38   );
39   foreach (keys %format) {
40     my %f = (%default, %{$format{$_}});
41     $format{$_} = $workbook->add_format(%f);
42     $format{"m_$_"} = $workbook->add_format(%f);
43   }
44
45   my ($r, $c) = (0, 0);
46   for my $row (@rows) {
47     $c = 0;
48     my $thisrow = shift @cells;
49     for my $cell (@$thisrow) {
50       if (!ref($cell)) {
51         # placeholder, so increment $c so that we write to the correct place
52         $c++;
53         next;
54       }
55       # format name
56       my $f = '';
57       $f = 'header' if $row->{header} or $cell->{header};
58       $f = 'money' if $cell->{format} eq 'money';
59       if ( $cell->{rowspan} > 1 or $cell->{colspan} > 1 ) {
60         my $range = xl_range_formula(
61           'Summary',
62           $r, $r - 1 + ($cell->{rowspan} || 1),
63           $c, $c - 1 + ($cell->{colspan} || 1)
64         );
65         #warn "merging $range\n";
66         $worksheet->merge_range($range, $cell->{value}, $format{"m_$f"});
67       } else {
68       #warn "writing ".xl_rowcol_to_cell($r, $c)."\n";
69         $worksheet->write( $r, $c, $cell->{value}, $format{$f} );
70       }
71       $c++;
72     } #$cell
73   $r++;
74   } #$row
75   $workbook->close;
76
77   http_header('Content-Length' => length($output));
78   $m->print($output);
79 </%perl>
80 % } else {
81 <& /elements/header.html, $title &>
82 % my $myself = $cgi->self_url;
83 <P ALIGN="right" CLASS="noprint">
84 Download full reports<BR>
85 as <A HREF="<% "$myself;_type=xls" %>">Excel spreadsheet</A><BR>
86 % # as <A HREF="<% "$myself;_type=csv" %>">CSV file</A> # is this still needed?
87 </P>
88 <style type="text/css">
89 .report * {
90   background-color: #f8f8f8;
91   border: 1px solid black;
92   padding: 2px;
93 }
94 .report td {
95   text-align: right;
96 }
97 .total * { background-color: #f5f6be; }
98 .shaded * { background-color: #c8c8c8; }
99 .totalshaded * { background-color: #bfc094; }
100 </style>
101 <table class="report" width="100%" cellspacing=0>
102 % foreach my $rowinfo (@rows) {
103   <tr<% $rowinfo->{class} ? ' class="'.$rowinfo->{class}.'"' : ''%>>
104 %   my $thisrow = shift @cells;
105 %   foreach my $cell (@$thisrow) {
106 %     next if !ref($cell); # placeholders
107 %     my $td = $cell->{header} ? 'th' : 'td';
108 %     my $style = '';
109 %     $style .= " rowspan=".$cell->{rowspan} if $cell->{rowspan} > 1;
110 %     $style .= " colspan=".$cell->{colspan} if $cell->{colspan} > 1;
111       <<%$td%><%$style%>><% $cell->{value} %></<%$td%>>
112 %   }
113   </tr>
114 % }
115 </table>
116
117 <& /elements/footer.html &>
118 % }
119 <%init>
120
121 die "access denied"
122   unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
123
124 my ($agentnum,$sel_agent);
125 if ( $cgi->param('agentnum') eq 'all' ) {
126   $agentnum = 0;
127 }
128 elsif ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
129   $agentnum = $1;
130   $sel_agent = qsearchs('agent', { 'agentnum' => $agentnum } );
131   die "agentnum $agentnum not found!" unless $sel_agent;
132 }
133 my $title = $sel_agent ? $sel_agent->agent.' ' : '';
134
135 my ($refnum,$sel_part_referral);
136 if ( $cgi->param('refnum') =~ /^(\d+)$/ ) {
137   $refnum = $1;
138   $sel_part_referral = qsearchs('part_referral', { 'refnum' => $refnum } );
139   die "refnum $refnum not found!" unless $sel_part_referral;
140 }
141 $title .=  $sel_part_referral->referral.' '
142   if $sel_part_referral;
143
144 $title .= 'Customer Accounting Summary Report';
145
146 my @custs = ();
147 @custs = qsearch('cust_main', {} );
148
149 my @items  = ('netsales', 'cashflow');
150 my @params = ( [], [] );
151 my $setuprecur = '';
152 if ( $cgi->param('setuprecur') ) {
153   $setuprecur = 1;
154   # instead of 'cashflow' (payments - refunds), use 'receipts'
155   # (applied payments), because it's divisible into setup and recur.
156   @items = ('netsales', 'receipts', 'netsales', 'receipts');
157   @params = ( 
158     [ setuprecur => 'setup' ],
159     [ setuprecur => 'setup' ],
160     [ setuprecur => 'recur' ],
161     [ setuprecur => 'recur' ],
162   );
163 }
164 my @labels = ();
165 my @cross_params = ();
166 my @custnames = ();
167
168 my $status = $cgi->param('status');
169 die "invalid status" unless $status =~ /^\w+|$/;
170
171 foreach my $cust_main ( @custs ) {
172   # XXX should do this in the qsearch
173   next unless ($status eq '' || $status eq $cust_main->status); 
174   next unless ($agentnum == 0 || $cust_main->agentnum eq $agentnum);
175   next unless ($refnum   == 0 || $cust_main->refnum eq $refnum);
176
177   push @custnames, $cust_main->name;
178
179   push @cross_params, [ ('custnum' => $cust_main->custnum) ];
180 }
181
182 my %opt = (
183   items         => \@items,
184   params        => \@params,
185   cross_params  => \@cross_params,
186   agentnum      => $agentnum,
187   refnum        => $refnum,
188 );
189 for ( qw(start_month start_year end_month end_year) ) {
190   if ( $cgi->param($_) =~ /^(\d+)$/ ) {
191     $opt{$_} = $1;
192   }
193 }
194
195 warn Dumper(OPTIONS => \%opt) if $cgi->param('debug');
196 my $report = FS::Report::Table::Monthly->new(%opt);
197 my $data = $report->data;
198 warn Dumper(DATA => $data) if $cgi->param('debug') >= 2;
199
200 my @total;
201
202 my @rows; # hashes of row info
203 my @cells; # arrayrefs of cell info
204 # We use Excel currency format, but not Excel dates, because
205 # these are whole months and there's no nice way to express that.
206 # This is the historical behavior for monthly reports.
207
208 # header row
209 $rows[0] = {};
210 $cells[0] = [
211   { header => 1, rowspan => 2, colspan => ($setuprecur ? 2 : 1) },
212   ($setuprecur ? '' : ()),
213   map {
214     { header => 1, colspan => 2, value => time2str('%b %Y', $_) },
215     ''
216   } @{ $data->{speriod} }
217 ];
218 my $ncols = scalar(@{ $data->{speriod} });
219
220 $rows[1] = {};
221 $cells[1] = [ '',
222   ($setuprecur ? '' : ()),
223   map { 
224   ( { header => 1, value => mt('Billed') },
225     { header => 1, value => mt('Paid') }
226   ) } (1..$ncols)
227 ];
228
229 # use PDL; # ha ha, I just might.
230 my $row = 0;
231 foreach my $name (@custnames) { # correspond to cross_params
232   my $skip = 1; # skip the customer iff ALL of their values are zero
233   for my $subrow (0..($setuprecur ? 1 : 0)) { # the setup/recur axis
234     push @rows, { class => $subrow ? 'shaded' : '' };
235     my @thisrow;
236     if ( $subrow == 0 ) {
237       # customer name
238       push @thisrow,
239         { value => $name,
240           header => 1,
241           rowspan => ($setuprecur ? 2 : 1) };
242     } else {
243       push @thisrow, '';
244     }
245     if ( $setuprecur ) {
246       # subheading
247       push @thisrow,
248         { value => $subrow ? mt('recurring') : mt('setup'),
249           header => 1 };
250     }
251     for my $col (0..$ncols-1) { # the month
252       for my $subcol (0..1) { # the billed/paid axis
253         my $item = $subrow * 2 + $subcol;
254         my $value = $data->{data}[$item][$col][$row];
255         $skip = 0 if abs($value) > 0.005;
256         push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
257         $total[( ($ncols * $subrow) + $col ) * 2 + $subcol] += $value;
258       } #subcol
259     } #col
260     push @cells, \@thisrow;
261   } #subrow
262   if ( $skip ) {
263     # all values are zero--remove the rows we just added
264     pop @rows;
265     pop @cells;
266     if ( $setuprecur ) {
267       pop @rows;
268       pop @cells;
269     }
270   }
271   $row++;
272 }
273 for my $subrow (0..($setuprecur ? 1 : 0)) {
274   push @rows, { class => ($subrow ? 'totalshaded' : 'total') };
275   my @thisrow;
276   if ( $subrow == 0 ) {
277     push @thisrow,
278       { value => mt('Total'),
279         header => 1,
280         rowspan => ($setuprecur ? 2 : 1), };
281   } else {
282     push @thisrow, '';
283   }
284   if ( $setuprecur ) {
285     push @thisrow,
286       { value => $subrow ? mt('recurring') : mt('setup'),
287         header => 1 };
288   }
289   for my $col (0..($ncols * 2)-1) { # month and billed/paid axis
290     my $value = $total[($subrow * $ncols * 2) + $col];
291     push @thisrow, { value => sprintf('%0.2f', $value), format => 'money' };
292   }
293   push @cells, \@thisrow;
294 } #subrow
295
296 if ( $cgi->param('debug') >= 3 ) {
297   warn Dumper(\@rows, \@cells);
298 }
299
300 my $title = 'Customer Accounting Summary';
301 </%init>