+my %condensed_format = (
+ 'label' => [ qw( Description Qty Amount ) ],
+ 'fields' => [
+ sub { shift->{description} },
+ sub { shift->{quantity} },
+ sub { my($href, %opt) = @_;
+ ($opt{dollar} || ''). $href->{amount};
+ },
+ ],
+ 'align' => [ qw( l r r ) ],
+ 'span' => [ qw( 5 1 1 ) ], # unitprices?
+ 'width' => [ qw( 10.7cm 1.4cm 1.6cm ) ], # don't like this
+);
+
+sub _condense_section {
+ my ( $self, $format ) = ( shift, shift );
+ ( 'condensed' => 1,
+ map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ );
+}
+
+sub _condensed_generator_defaults {
+ my ( $self, $format ) = ( shift, shift );
+ return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
+}
+
+my %html_align = (
+ 'c' => 'center',
+ 'l' => 'left',
+ 'r' => 'right',
+);
+
+sub _condensed_header_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
+ $suffix = "\\\\\n\\hline";
+ $separator = "&\n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ } elsif ( $format eq 'html' ) {
+ $prefix = '<th></th>';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<th align="$html_align{$a}">$d</th>!;
+ };
+ }
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
+ }
+
+ $prefix. join($separator, @result). $suffix;
+ };
+
+}
+
+sub _condensed_description_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+
+ my $money_char = '$';
+ if ($format eq 'latex') {
+ $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
+ $suffix = '\\\\';
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+ };
+ $money_char = '\\dollar';
+ }elsif ( $format eq 'html' ) {
+ $prefix = '"><td align="center"></td>';
+ $suffix = '';
+ $separator = '';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}">$d</td>!;
+ };
+ #$money_char = $conf->config('money_char') || '$';
+ $money_char = ''; # this is madness
+ }
+
+ sub {
+ #my @args = @_;
+ my $href = shift;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ my $dollar = '';
+ $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+sub _condensed_total_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ # my $r = &{$f->{fields}->[$i]}(@args);
+ # $r .= ' Total' unless $i;
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+=item total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class. FORMAT is either html or latex
+
+=cut
+
+# should not be used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub _condensed_total_line_generator {
+ my ( $self, $format ) = ( shift, shift );
+
+ my ( $f, $prefix, $suffix, $separator, $column ) =
+ _condensed_generator_defaults($format);
+ my $style = '';
+
+ if ($format eq 'latex') {
+ $prefix = "& ";
+ $suffix = "\\\\\n";
+ $separator = " & \n";
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+ };
+ }elsif ( $format eq 'html' ) {
+ $prefix = '';
+ $suffix = '';
+ $separator = '';
+ $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+ $column =
+ sub { my ($d,$a,$s,$w) = @_;
+ return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+ };
+ }
+
+
+ sub {
+ my @args = @_;
+ my @result = ();
+
+ foreach (my $i = 0; $f->{label}->[$i]; $i++) {
+ push @result,
+ &{$column}( &{$f->{fields}->[$i]}(@args),
+ map { $f->{$_}->[$i] } qw(align span width)
+ );
+ }
+
+ $prefix. join( $separator, @result ). $suffix;
+ };
+
+}
+
+#sub _items_extra_usage_sections {
+# my $self = shift;
+# my $escape = shift;
+#
+# my %sections = ();
+#
+# my %usage_class = map{ $_->classname, $_ } qsearch('usage_class', {});
+# foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+# {
+# next unless $cust_bill_pkg->pkgnum > 0;
+#
+# foreach my $section ( keys %usage_class ) {
+#
+# my $usage = $cust_bill_pkg->usage($section);
+#
+# next unless $usage && $usage > 0;
+#
+# $sections{$section} ||= 0;
+# $sections{$section} += $usage;
+#
+# }
+#
+# }
+#
+# map { { 'description' => &{$escape}($_),
+# 'subtotal' => $sections{$_},
+# 'summarized' => '',
+# 'tax_section' => '',
+# }
+# }
+# sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
+
+sub _items_extra_usage_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ foreach my $classnum ( keys %usage_class ) {
+ my $section = $usage_class{$classnum}->classname;
+ $classnums{$section} = $classnum;
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+ $sections{$section}{amount} += $amount; #subtotal
+ $sections{$section}{calls}++;
+ $sections{$section}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
+
+ $lines{$section}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => $cust_bill_pkg->pkgnum,
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => $cust_bill_pkg->unitrecur,
+ quantity => $cust_bill_pkg->quantity,
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$section}{$desc}{amount} += $amount;
+ $lines{$section}{$desc}{calls}++;
+ $lines{$section}{$desc}{duration} += $detail->duration;
+
+ }
+ }
+ }
+
+ my %sectionmap = ();
+ foreach (keys %sections) {
+ my $usage_class = $usage_class{$classnums{$_}};
+ $sectionmap{$_} = { 'description' => &{$escape}($_),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'sort_weight' => $usage_class->weight,
+ ( $usage_class->format
+ ? ( map { $_ => $usage_class->$_($format) }
+ qw( description_generator header_generator total_generator total_line_generator )
+ )
+ : ()
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ return(\@sections, \@lines);
+
+}
+
+sub _did_summary {
+ my $self = shift;
+ my $end = $self->_date;
+
+ # start at date of previous invoice + 1 second or 0 if no previous invoice
+ my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
+ $start = 0 if !$start;
+ $start++;
+
+ my $cust_main = $self->cust_main;
+ my @pkgs = $cust_main->all_pkgs;
+ my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+ = (0,0,0,0,0);
+ my @seen = ();
+ foreach my $pkg ( @pkgs ) {
+ my @h_cust_svc = $pkg->h_cust_svc($end);
+ foreach my $h_cust_svc ( @h_cust_svc ) {
+ next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+ next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+
+ my $inserted = $h_cust_svc->date_inserted;
+ my $deleted = $h_cust_svc->date_deleted;
+ my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
+ my $phone_deleted;
+ $phone_deleted = $h_cust_svc->h_svc_x($deleted) if $deleted;
+
+# DID either activated or ported in; cannot be both for same DID simultaneously
+ if ($inserted >= $start && $inserted <= $end && $phone_inserted
+ && (!$phone_inserted->lnp_status
+ || $phone_inserted->lnp_status eq ''
+ || $phone_inserted->lnp_status eq 'native')) {
+ $num_activated++;
+ }
+ else { # this one not so clean, should probably move to (h_)svc_phone
+ my $phone_portedin = qsearchs( 'h_svc_phone',
+ { 'svcnum' => $h_cust_svc->svcnum,
+ 'lnp_status' => 'portedin' },
+ FS::h_svc_phone->sql_h_searchs($end),
+ );
+ $num_portedin++ if $phone_portedin;
+ }
+
+# DID either deactivated or ported out; cannot be both for same DID simultaneously
+ if($deleted >= $start && $deleted <= $end && $phone_deleted
+ && (!$phone_deleted->lnp_status
+ || $phone_deleted->lnp_status ne 'portingout')) {
+ $num_deactivated++;
+ }
+ elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+ && $phone_deleted->lnp_status
+ && $phone_deleted->lnp_status eq 'portingout') {
+ $num_portedout++;
+ }
+
+ # increment usage minutes
+ if ( $phone_inserted ) {
+ my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
+ $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
+ }
+ else {
+ warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
+ }
+
+ # don't look at this service again
+ push @seen, $h_cust_svc->svcnum;
+ }
+ }
+
+ $minutes = sprintf("%d", $minutes);
+ ("Activated: $num_activated Ported-In: $num_portedin Deactivated: "
+ . "$num_deactivated Ported-Out: $num_portedout ",
+ "Total Minutes: $minutes");
+}
+
+sub _items_svc_phone_sections {
+ my $self = shift;
+ my $escape = shift;
+ my $format = shift;
+
+ my %sections = ();
+ my %classnums = ();
+ my %lines = ();
+
+ my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+
+ my %usage_class = map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+ $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
+
+ foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+ next unless $cust_bill_pkg->pkgnum > 0;
+
+ my @header = $cust_bill_pkg->details_header;
+ next unless scalar(@header);
+
+ foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+ my $phonenum = $detail->phonenum;
+ next unless $phonenum;
+
+ my $amount = $detail->amount;
+ next unless $amount && $amount > 0;
+
+ $sections{$phonenum} ||= { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => -1,
+ 'phonenum' => $phonenum,
+ };
+ $sections{$phonenum}{amount} += $amount; #subtotal
+ $sections{$phonenum}{calls}++;
+ $sections{$phonenum}{duration} += $detail->duration;
+
+ my $desc = $detail->regionname;
+ my $description = $desc;
+ $description = substr($desc, 0, $maxlength). '...'
+ if $format eq 'latex' && length($desc) > $maxlength;
+
+ $lines{$phonenum}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{$phonenum}{$desc}{amount} += $amount;
+ $lines{$phonenum}{$desc}{calls}++;
+ $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+ my $line = $usage_class{$detail->classnum}->classname;
+ $sections{"$phonenum $line"} ||=
+ { 'amount' => 0,
+ 'calls' => 0,
+ 'duration' => 0,
+ 'sort_weight' => $usage_class{$detail->classnum}->weight,
+ 'phonenum' => $phonenum,
+ 'header' => [ @header ],
+ };
+ $sections{"$phonenum $line"}{amount} += $amount; #subtotal
+ $sections{"$phonenum $line"}{calls}++;
+ $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+ $lines{"$phonenum $line"}{$desc} ||= {
+ description => &{$escape}($description),
+ #pkgpart => $part_pkg->pkgpart,
+ pkgnum => '',
+ ref => '',
+ amount => 0,
+ calls => 0,
+ duration => 0,
+ #unit_amount => '',
+ quantity => '',
+ product_code => 'N/A',
+ ext_description => [],
+ };
+
+ $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+ $lines{"$phonenum $line"}{$desc}{calls}++;
+ $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+ push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+ $detail->formatted('format' => $format);
+
+ }
+ }
+
+ my %sectionmap = ();
+ my $simple = new FS::usage_class { format => 'simple' }; #bleh
+ foreach ( keys %sections ) {
+ my @header = @{ $sections{$_}{header} || [] };
+ my $usage_simple =
+ new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
+ my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+ my $usage_class = $summary ? $simple : $usage_simple;
+ my $ending = $summary ? ' usage charges' : '';
+ my %gen_opt = ();
+ unless ($summary) {
+ $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+ }
+ $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+ 'amount' => $sections{$_}{amount}, #subtotal
+ 'calls' => $sections{$_}{calls},
+ 'duration' => $sections{$_}{duration},
+ 'summarized' => '',
+ 'tax_section' => '',
+ 'phonenum' => $sections{$_}{phonenum},
+ 'sort_weight' => $sections{$_}{sort_weight},
+ 'post_total' => $summary, #inspire pagebreak
+ (
+ ( map { $_ => $usage_class->$_($format, %gen_opt) }
+ qw( description_generator
+ header_generator
+ total_generator
+ total_line_generator
+ )
+ )
+ ),
+ };
+ }
+
+ my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+ $a->{sort_weight} <=> $b->{sort_weight}
+ }
+ values %sectionmap;
+
+ my @lines = ();
+ foreach my $section ( keys %lines ) {
+ foreach my $line ( keys %{$lines{$section}} ) {
+ my $l = $lines{$section}{$line};
+ $l->{section} = $sectionmap{$section};
+ $l->{amount} = sprintf( "%.2f", $l->{amount} );
+ #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+ push @lines, $l;
+ }
+ }
+
+ if($conf->exists('phone_usage_class_summary')) {
+ # this only works with Latex
+ my @newlines;
+ my @newsections;
+
+ # after this, we'll have only two sections per DID:
+ # Calls Summary and Calls Detail
+ foreach my $section ( @sections ) {
+ if($section->{'post_total'}) {
+ $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+ $section->{'total_line_generator'} = sub { '' };
+ $section->{'total_generator'} = sub { '' };
+ $section->{'header_generator'} = sub { '' };
+ $section->{'description_generator'} = '';
+ push @newsections, $section;
+ my %calls_detail = %$section;
+ $calls_detail{'post_total'} = '';
+ $calls_detail{'sort_weight'} = '';
+ $calls_detail{'description_generator'} = sub { '' };
+ $calls_detail{'header_generator'} = sub {
+ return ' & Date/Time & Called Number & Duration & Price'
+ if $format eq 'latex';
+ '';
+ };
+ $calls_detail{'description'} = 'Calls Detail: '
+ . $section->{'phonenum'};
+ push @newsections, \%calls_detail;
+ }
+ }
+
+ # after this, each usage class is collapsed/summarized into a single
+ # line under the Calls Summary section
+ foreach my $newsection ( @newsections ) {
+ if($newsection->{'post_total'}) { # this means Calls Summary
+ foreach my $section ( @sections ) {
+ next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+ && !$section->{'post_total'});
+ my $newdesc = $section->{'description'};
+ my $tn = $section->{'phonenum'};
+ $newdesc =~ s/$tn//g;
+ my $line = { ext_description => [],
+ pkgnum => '',
+ ref => '',
+ quantity => '',
+ calls => $section->{'calls'},
+ section => $newsection,
+ duration => $section->{'duration'},
+ description => $newdesc,
+ amount => sprintf("%.2f",$section->{'amount'}),
+ product_code => 'N/A',
+ };
+ push @newlines, $line;
+ }
+ }
+ }
+
+ # after this, Calls Details is populated with all CDRs
+ foreach my $newsection ( @newsections ) {
+ if(!$newsection->{'post_total'}) { # this means Calls Details
+ foreach my $line ( @lines ) {
+ next unless (scalar(@{$line->{'ext_description'}}) &&
+ $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
+ );
+ my @extdesc = @{$line->{'ext_description'}};
+ my @newextdesc;
+ foreach my $extdesc ( @extdesc ) {
+ $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
+ push @newextdesc, $extdesc;
+ }
+ $line->{'ext_description'} = \@newextdesc;
+ $line->{'section'} = $newsection;
+ push @newlines, $line;
+ }
+ }
+ }
+
+ return(\@newsections, \@newlines);
+ }
+
+ return(\@sections, \@lines);
+
+}
+