1 <& elements/search.html,
2 'title' => emt('Sales with advertising source'),
3 'name' => emt('line items'),
5 'count_query' => $count_query,
7 ($setup ? $money_char. '%.2f setup' : ()),
8 ($recur ? $money_char. '%.2f recurring' : ()),
9 ($usage ? $money_char. '%.2f usage' : ()),
13 ($setup ? emt('Setup') : ()),
14 ($recur ? emt('Recurring') : ()),
15 ($usage ? emt('Usage') : ()),
30 ($setup ? money_sub('setup') : ()),
31 ($recur ? money_sub('recur_no_usage') : ()),
32 ($usage ? money_sub('recur_usage') : ()),
38 my $cust_pkg = shift->cust_pkg;
39 $cust_pkg ? ucfirst($cust_pkg->status) : '';
43 my $cust_bill_pkg = shift;
44 my $pkgpart = $cust_bill_pkg->pkgpart_override;
46 my $cust_pkg = $cust_bill_pkg->cust_pkg or return '';
47 $pkgpart = $cust_pkg->pkgpart;
49 if ( !exists($report_classes{$pkgpart}) ) {
50 my $part_pkg = FS::part_pkg->by_key($pkgpart);
51 my %opts = $part_pkg->options;
52 $report_classes{$pkgpart} = [
53 map { /^report_option_(\d+)/ ?
54 $report_option_name{$1} :
59 join( '<BR>', @{ $report_classes{$pkgpart} });
63 'referral', # from query
68 ($setup ? 'setup' : ()),
69 ($recur ? 'recur_no_usage' : ()),
70 ($usage ? 'recur_usage' : ()),
84 '', #package/item desc
85 ('') x $x, #setup/recur/usage
94 $clink, #customer name
98 #'align' => 'rlrrrc'.FS::UI::Web::cust_aligns(),
99 'align' => 'l' . ('r' x $x) . 'rcrccccrlll',
100 'color' => [ ('') x (5 + $x),
102 my $cust_pkg = shift->cust_pkg;
103 $cust_pkg ? $cust_pkg->statuscolor : '';
116 unless $FS::CurrentUser::CurrentUser->access_right('Financial reports');
118 my $conf = new FS::Conf;
120 my $setup = $cgi->param('setup') ? 1 : 0;
121 my $recur = $cgi->param('recur') ? 1 : 0;
122 my $usage = $cgi->param('usage') ? 1 : 0;
124 my $x = $setup + $recur + $usage;
126 my @select = ( 'cust_bill_pkg.*', 'cust_bill._date' );
127 my ($join_cust, $join_pkg ) = ('', '');
129 #here is the agent virtualization
131 $FS::CurrentUser::CurrentUser->agentnums_sql( 'table' => 'cust_main' );
133 my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
135 my @where = ( $agentnums_sql,
137 '(cust_bill_pkg.pkgnum != 0 OR '.
138 'cust_bill_pkg.feepart IS NOT NULL)',
139 "cust_bill._date >= $beginning",
140 "cust_bill._date <= $ending",
144 foreach my $status ($cgi->param('status')) {
145 if ( $status =~ /^([- a-z]+)$/ ) { #"one-time charge"
146 push @status_where, "'$status'";
149 if ( @status_where ) {
150 push @where, '('. FS::cust_pkg->status_sql.
151 ') IN (' . join(',', @status_where) .')';
155 foreach my $refnum ($cgi->param('refnum')) {
156 if ( $refnum =~ /^\d+$/ ) {
157 push @refnum, $refnum;
161 push @where, 'cust_main.refnum IN ('.join(',', @refnum).')';
164 # cust_classnum (false laziness w/ elements/cust_main_dayranges.html, elements/cust_pay_or_refund.html, prepaid_income.html, cust_bill_pay.html, cust_bill_pkg.html, unearned_detail.html, cust_credit.html, cust_credit_refund.html, cust_main::Search::search_sql)
165 if ( grep { $_ eq 'cust_classnum' } $cgi->param ) {
166 my @classnums = grep /^\d*$/, $cgi->param('cust_classnum');
167 push @where, 'COALESCE( cust_main.classnum, 0) IN ( '.
168 join(',', map { $_ || '0' } @classnums ).
173 if ( $cgi->param('agentnum') =~ /^(\d+)$/ ) {
174 push @where, "cust_main.agentnum = $1";
178 # not specified: all classes
181 my $use_override = 1; #$cgi->param('use_override');
182 if ( $cgi->param('classnum') =~ /^(\d+)$/ ) {
185 $comparison = "IS NULL";
187 $comparison = "= $1";
190 if ( $use_override ) {
192 (part_pkg.classnum $comparison AND pkgpart_override IS NULL) OR
193 (override.classnum $comparison AND pkgpart_override IS NOT NULL) OR
194 (part_fee.classnum $comparison AND feepart IS NOT NULL)
198 "(part_pkg.classnum $comparison) OR (part_fee.classnum $comparison)";
203 my @report_option = grep /^\d+$/, ( $cgi->param('report_option') );
204 if ( @report_option ) {
205 @report_option = map { "'report_option_$_'" } @report_option;
206 push @where, "EXISTS(
207 SELECT 1 FROM part_pkg_option WHERE optionname IN (".
208 join(',', @report_option).") AND (
209 part_pkg_option.pkgpart = cust_pkg.pkgpart AND pkgpart_override IS NULL
210 OR part_pkg_option.pkgpart = pkgpart_override
216 FS::cust_bill_pkg->charged_sql('', '', setuprecur => 'setup');
218 FS::cust_bill_pkg->charged_sql('', '', setuprecur => 'recur', no_usage => 1);
219 my $usage_sql = FS::cust_bill_pkg->usage_sql;
221 # exclude zero-amount items
223 push @orwhere, "(cust_bill_pkg.setup > 0)" if $setup;
224 push @orwhere, "($recur_sql > 0)" if $recur;
225 push @orwhere, "($usage_sql > 0)" if $usage;
226 push @where, '('.join(' OR ', @orwhere).')' if @orwhere;
228 $join_cust = ' JOIN cust_bill USING ( invnum )
229 LEFT JOIN cust_main USING ( custnum )
230 LEFT JOIN part_referral USING ( refnum )
231 LEFT JOIN agent ON cust_main.agentnum = agent.agentnum
234 $join_pkg .= ' LEFT JOIN cust_pkg USING ( pkgnum )
235 LEFT JOIN part_pkg USING ( pkgpart )
236 LEFT JOIN part_pkg AS override
237 ON pkgpart_override = override.pkgpart
238 LEFT JOIN part_fee USING ( feepart )
239 LEFT JOIN pkg_class ON '; #...
241 if ( $use_override ) {
242 # join to whichever pkgpart is appropriate
244 ( pkgpart_override IS NULL AND part_pkg.classnum = pkg_class.classnum )
245 OR ( pkgpart_override IS NOT NULL AND override.classnum = pkg_class.classnum )
246 OR ( feepart IS NOT NULL AND part_fee.classnum = pkg_class.classnum )';
249 ( part_pkg.classnum = pkg_class.classnum )
250 OR ( part_fee.classnum = pkg_class.classnum )';
253 my $where = ' WHERE '. join(' AND ', @where);
255 # setup and recurring only
256 my $count_query = "SELECT
258 ($setup ? ", SUM($setup_sql)" : '').
259 ($recur ? ", SUM($recur_sql)" : '').
260 ($usage ? ", SUM($usage_sql)" : '').
267 my $paid_sql = FS::cust_bill_pkg->paid_sql('', '');
268 my $last_pay_sql = "SELECT MAX(_date)
269 FROM cust_bill_pay JOIN cust_bill_pay_pkg USING (billpaynum)
270 WHERE cust_bill_pay_pkg.billpkgnum = cust_bill_pkg.billpkgnum";
272 push @select, 'COALESCE(part_pkg.pkg, part_fee.itemdesc) AS pkg',
278 'part_referral.referral',
279 "($paid_sql) AS paid",
280 "($last_pay_sql) AS last_pay",
281 "($recur_sql) AS recur_no_usage",
282 "($usage_sql) AS recur_usage",
283 'pkg_class.classname',
288 'table' => 'cust_bill_pkg',
289 'addl_from' => "$join_cust $join_pkg",
291 'select' => join(",\n", @select ),
292 'extra_sql' => $where,
293 'order_by' => 'ORDER BY cust_bill._date, billpkgnum',
296 my $ilink = [ "${p}view/cust_bill.cgi?", 'invnum' ];
297 my $clink = [ "${p}view/cust_main.cgi?", 'custnum' ];
299 my $conf = new FS::Conf;
300 my $money_char = $conf->config('money_char') || '$';
302 my %report_classes; #cache
303 my %report_option_name =
304 map { $_->num => $_->name } qsearch('part_pkg_report_option', {});
306 # should this be in Mason.pm or something?
308 $conf ||= new FS::Conf;
309 $money_char ||= $conf->config('money_char') || '$';
312 $money_char . sprintf('%.2f', $_[0]->get($field));
319 my $value = $_[0]->get($field);
320 $value ? time2str('%b %d %Y', $value) : '';