invoice formatting: add sections for usage, add sections per svc_phone, add folding...
authorjeff <jeff>
Fri, 20 Nov 2009 17:33:40 +0000 (17:33 +0000)
committerjeff <jeff>
Fri, 20 Nov 2009 17:33:40 +0000 (17:33 +0000)
12 files changed:
FS/FS/Conf.pm
FS/FS/Schema.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_detail.pm
FS/FS/part_pkg/voip_cdr.pm
FS/FS/usage_class.pm
conf/invoice_html
httemplate/browse/pkg_category.html
httemplate/browse/usage_class.html
httemplate/edit/pkg_category.html
httemplate/edit/usage_class.html

index 3df2326..d186d04 100644 (file)
@@ -1036,6 +1036,20 @@ worry that config_items is freeside-specific and icky.
     'type'        => 'checkbox',
   },
 
+  { 
+    'key'         => 'usage_class_as_a_section',
+    'section'     => 'billing',
+    'description' => 'Split usage into sections and label according to usage class name when enabled.  Only valid when invoice_sections is enabled.',
+    'type'        => 'checkbox',
+  },
+
+  { 
+    'key'         => 'svc_phone_sections',
+    'section'     => 'billing',
+    'description' => 'Create a section for each svc_phone when enabled.  Only valid when invoice_sections is enabled.',
+    'type'        => 'checkbox',
+  },
+
   {
     'key'         => 'finance_pkgclass',
     'section'     => 'billing',
index f4cbe1d..553ddc2 100644 (file)
@@ -573,7 +573,9 @@ sub tables_hashref {
         'amount',  'decimal', 'NULL', '10,4', '', '',
         'format',  'char', 'NULL', 1, '', '',
         'classnum', 'int', 'NULL', '', '', '',
+        'duration', 'int', 'NULL', '',  0, '',
         'phonenum', 'varchar', 'NULL', 15, '', '',
+        'regionname', 'varchar', 'NULL', $char_d, '', '',
         'detail',  'varchar', '', 255, '', '', 
       ],
       'primary_key' => 'detailnum',
@@ -2010,9 +2012,11 @@ sub tables_hashref {
 
     'usage_class' => {
       'columns' => [
-        'classnum',    'serial',   '',      '', '', '', 
-        'classname',   'varchar',  '', $char_d, '', '', 
-        'disabled',    'char', 'NULL',       1, '', '', 
+        'classnum',    'serial',      '',      '', '', '', 
+        'weight',      'int',     'NULL',      '', '', '',
+        'classname',   'varchar',     '', $char_d, '', '', 
+        'format',      'varchar', 'NULL', $char_d, '', '', 
+        'disabled',    'char',    'NULL',       1, '', '', 
       ],
       'primary_key' => 'classnum',
       'unique' => [],
@@ -2125,6 +2129,7 @@ sub tables_hashref {
         'categorynum',   'serial',  '', '', '', '', 
         'categoryname',  'varchar', '', $char_d, '', '', 
         'weight',         'int', 'NULL',  '', '', '',
+        'condense',      'char', 'NULL',   1, '', '', 
         'disabled',      'char', 'NULL',   1, '', '', 
       ],
       'primary_key' => 'categorynum',
index 82fa78a..cf339a8 100644 (file)
@@ -11,6 +11,7 @@ use File::Temp 0.14;
 use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
+use Storable qw( freeze thaw );
 use FS::UID qw( datasrc );
 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
 use FS::Record qw( qsearch qsearchs dbh );
@@ -19,6 +20,7 @@ use FS::cust_main;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_detail;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
@@ -2474,8 +2476,24 @@ sub print_generic {
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
   my $late_sections = [];
   if ( $multisection ) {
+    my ($extra_sections, $extra_lines) =
+      $self->_items_extra_usage_sections($escape_function, $format)
+      if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
+
+    push @detail_items, @$extra_lines if $extra_lines;
     push @sections,
-      $self->_items_sections( $late_sections, $summarypage, $escape_function );
+      $self->_items_sections( $late_sections,      # this could stand a refactor
+                              $summarypage,
+                              $escape_function,
+                              $extra_sections,
+                              $format,             #bah
+                            );
+    if ($conf->exists('svc_phone_sections')) {
+      my ($phone_sections, $phone_lines) =
+        $self->_items_svc_phone_sections($escape_function, $format);
+      push @{$late_sections}, @$phone_sections;
+      push @detail_items, @$phone_lines;
+    }
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
@@ -2528,6 +2546,11 @@ sub print_generic {
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
 
+    # begin some normalization
+    $section->{'amount'}   = $section->{'subtotal'}
+      if $multisection;
+
+
     if ( $section->{'description'} ) {
       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
                    [ '', '' ],
@@ -3115,6 +3138,8 @@ sub _items_sections {
   my $late = shift;
   my $summarypage = shift;
   my $escape = shift;
+  my $extra_sections = shift;
+  my $format = shift;
 
   my %subtotal = ();
   my %late_subtotal = ();
@@ -3188,8 +3213,13 @@ sub _items_sections {
   push @$late, map { { 'description' => &{$escape}($_),
                        'subtotal'    => $late_subtotal{$_},
                        'post_total'  => 1,
+                       'sort_weight' => _pkg_category($_)->weight,
+                       (_pkg_category($_)->condense
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
                    } }
-                 sort _categorysort keys %late_subtotal;
+                 sort _sectionsort keys %late_subtotal;
 
   my @sections;
   if ( $summarypage ) {
@@ -3199,19 +3229,26 @@ sub _items_sections {
     @sections = keys %subtotal;
   }
 
-  map { { 'description' => &{$escape}($_),
-          'subtotal'    => $subtotal{$_},
-          'summarized'  => $not_tax{$_} ? '' : 'Y',
-          'tax_section' => $not_tax{$_} ? '' : 'Y',
-        }
-      }
-    sort _categorysort @sections;
+  my @early = map { { 'description' => &{$escape}($_),
+                      'subtotal'    => $subtotal{$_},
+                      'summarized'  => $not_tax{$_} ? '' : 'Y',
+                      'tax_section' => $not_tax{$_} ? '' : 'Y',
+                      'sort_weight' => _pkg_category($_)->weight,
+                       (_pkg_category($_)->condense
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
+                    }
+                  } @sections;
+  push @early, @$extra_sections if $extra_sections;
+  sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
 
 }
 
 #helper subs for above
 
-sub _categorysort {
+sub _sectionsort {
   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
 }
 
@@ -3221,6 +3258,478 @@ sub _pkg_category {
     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
 }
 
+my %condensed_format = (
+  'label' => [ qw( Description Qty Amount ) ],
+  'fields' => [
+                sub { shift->{description} },
+                sub { shift->{quantity} },
+                sub { shift->{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);
+
+  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}}}";
+          };
+  }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>!;
+      };
+  }
+
+  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 _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 %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, 50). '...'
+          if $format eq 'latex' && length($desc) > 50;
+
+        $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 _items_svc_phone_sections {
+  my $self = shift;
+  my $escape = shift;
+  my $format = shift;
+
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
+
+  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 $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, 50). '...'
+        if $format eq 'latex' && length($desc) > 50;
+
+      $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,
+        };
+      $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
+  my $minimal = new FS::usage_class { format => 'minimal' }; #bleh
+  foreach ( keys %sections ) {
+    my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+    my $usage_class = $summary ? $simple : $minimal;
+    my $ending = $summary ? ' usage charges' : '';
+    $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},
+                        (
+                          ( map { $_ => $usage_class->$_($format) }
+                            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;
+    }
+  }
+
+  return(\@sections, \@lines);
+
+}
+
 sub _items {
   my $self = shift;
 
@@ -3270,8 +3779,30 @@ sub _items_previous {
 
 sub _items_pkg {
   my $self = shift;
+  my %options = @_;
   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+  if ($options{section} && $options{section}->{condensed}) {
+    my %itemshash = ();
+    local $Storable::canonical = 1;
+    foreach ( @items ) {
+      my $item = { %$_ };
+      delete $item->{ref};
+      delete $item->{ext_description};
+      my $key = freeze($item);
+      $itemshash{$key} ||= 0;
+      $itemshash{$key} ++; # += $item->{quantity};
+    }
+    @items = sort { $a->{description} cmp $b->{description} }
+             map { my $i = thaw($_);
+                   $i->{quantity} = $itemshash{$_};
+                   $i->{amount} =
+                     sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
+                   $i;
+                 }
+             keys %itemshash;
+  }
+  @items;
 }
 
 sub _taxsort {
index 094313f..7d5094c 100644 (file)
@@ -152,6 +152,8 @@ sub insert {
         'amount'     => (ref($detail) ? $detail->[2] : '' ),
         'classnum'   => (ref($detail) ? $detail->[3] : '' ),
         'phonenum'   => (ref($detail) ? $detail->[4] : '' ),
+        'duration'   => (ref($detail) ? $detail->[5] : '' ),
+        'regionname' => (ref($detail) ? $detail->[6] : '' ),
       };
       $error = $cust_bill_pkg_detail->insert;
       if ( $error ) {
@@ -800,6 +802,25 @@ sub cust_bill_pkg_tax_Xlocation {
 
 }
 
+=item cust_bill_pkg_detail [ CLASSNUM ]
+
+Returns the list of associated cust_bill_pkg_detail objects
+The optional CLASSNUM argument will limit the details to the specified usage
+class.
+
+=cut
+
+sub cust_bill_pkg_detail {
+  my $self = shift;
+  my $classnum = shift || '';
+
+  my %hash = ( 'billpkgnum' => $self->billpkgnum );
+  $hash{classnum} = $classnum if $classnum;
+
+  qsearch ( 'cust_bill_pkg_detail', { %hash  } ),
+
+}
+
 =back
 
 =head1 BUGS
index f8c9d1a..63d0ac5 100644 (file)
@@ -2,6 +2,7 @@ package FS::cust_bill_pkg_detail;
 
 use strict;
 use vars qw( @ISA $me $DEBUG %GetInfoType );
+use HTML::Entities;
 use FS::Record qw( qsearch qsearchs dbdef dbh );
 use FS::cust_bill_pkg;
 use FS::Conf;
@@ -41,6 +42,18 @@ inherits from FS::Record.  The following fields are currently supported:
 
 =item billpkgnum - link to cust_bill_pkg
 
+=item amount - price of this line item detail
+
+=item format - '' for straight text and 'C' for CSV in detail
+
+=item classnum - link to usage_class
+
+=item duration - granularized number of seconds for this call
+
+=item regionname -
+
+=item phonenum -
+
 =item detail - detail description
 
 =back
@@ -121,6 +134,8 @@ sub check {
     #|| $self->ut_moneyn('amount')
     || $self->ut_floatn('amount')
     || $self->ut_enum('format', [ '', 'C' ] )
+    || $self->ut_numbern('duration')
+    || $self->ut_textn('regionname')
     || $self->ut_text('detail')
     || $self->ut_foreign_keyn('classnum', 'usage_class', 'classnum')
     || $self->$phonenum_check_method('phonenum')
@@ -129,6 +144,92 @@ sub check {
 
 }
 
+=item formatted [ OPTION => VALUE ... ]
+
+Returns detail information for the invoice line item detail formatted for
+display.
+
+Currently available options are: I<format> I<escape_function>
+
+If I<format> is set to html or latex then the format is improved
+for tabular appearance in those environments if possible.
+
+If I<escape_function> is set then the format is processed by this
+function before being returned.
+
+If I<format_function> is set then the detail is handed to this callback
+for processing.
+
+=cut
+
+sub formatted {
+  my ( $self, %opt ) = @_;
+  my $format = $opt{format} || '';
+  return () unless defined dbdef->table('cust_bill_pkg_detail');
+
+  eval "use Text::CSV_XS;";
+  die $@ if $@;
+  my $csv = new Text::CSV_XS;
+
+  my $escape_function = sub { shift };
+
+  $escape_function = \&encode_entities
+    if $format eq 'html';
+
+  $escape_function =
+    sub {
+      my $value = shift;
+      $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
+      $value =~ s/([<>])/\$$1\$/g;
+      $value;
+    }
+  if $format eq 'latex';
+
+  $escape_function = $opt{escape_function} if $opt{escape_function};
+
+  my $format_sub = sub { my $detail = shift;
+                         $csv->parse($detail) or return "can't parse $detail";
+                         join(' - ', map { &$escape_function($_) }
+                                     $csv->fields
+                             );
+                       };
+
+  $format_sub = sub { my $detail = shift;
+                      $csv->parse($detail) or return "can't parse $detail";
+                      join('</TD><TD>', map { &$escape_function($_) }
+                                        $csv->fields
+                          );
+                    }
+    if $format eq 'html';
+
+  $format_sub = sub { my $detail = shift;
+                      $csv->parse($detail) or return "can't parse $detail";
+                      #join(' & ', map { '\small{'. &$escape_function($_). '}' }                      #            $csv->fields );
+                      my $result = '';
+                      my $column = 1;
+                      foreach ($csv->fields) {
+                        $result .= ' & ' if $column > 1;
+                        if ($column > 6) {                     # KLUDGE ALERT!
+                          $result .= '\multicolumn{1}{l}{\scriptsize{'.
+                                     &$escape_function($_). '}}';
+                        }else{
+                          $result .= '\scriptsize{'.  &$escape_function($_). '}';
+                        }
+                        $column++;
+                      }
+                      $result;
+                    }
+    if $format eq 'latex';
+
+  $format_sub = $opt{format_function} if $opt{format_function};
+
+  $self->format eq 'C'
+    ? &{$format_sub}( $self->detail, $self )
+    : &{$escape_function}( $self->detail )
+  ;
+}
+
+
 # _upgrade_data
 #
 # Used by FS::Upgrade to migrate to a new database.
index eccf2c1..47102c1 100644 (file)
@@ -15,7 +15,7 @@ use FS::part_pkg::recur_Common;
 
 @ISA = qw(FS::part_pkg::recur_Common);
 
-$DEBUG = 0;
+$DEBUG = 1;
 
 tie my %rating_method, 'Tie::IxHash',
   'prefix' => 'Rate calls by using destination prefix to look up a region and rate according to the internal prefix and rate tables',
@@ -324,6 +324,8 @@ sub calc_usage {
       my( $rate_region, $regionnum );
       my $pretty_destnum;
       my $charge = '';
+      my $seconds = '';
+      my $regionname = '';
       my $classnum = '';
       my @call_details = ();
       if ( $rating_method eq 'prefix' ) {
@@ -409,6 +411,7 @@ sub calc_usage {
 
             $rate_region = $rate_detail->dest_region;
             $regionnum = $rate_region->regionnum;
+            $regionname = $rate_region->regionname;
             warn "  found rate for regionnum $regionnum ".
                  "and rate detail $rate_detail\n"
               if $DEBUG;
@@ -481,7 +484,7 @@ sub calc_usage {
                             : 60;
 
                     # length($cdr->billsec) ? $cdr->billsec : $cdr->duration;
-        my $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
+        $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
 
         $seconds += $granularity - ( $seconds % $granularity )
           if $seconds      # don't granular-ize 0 billsec calls (bills them)
@@ -530,7 +533,7 @@ sub calc_usage {
           my $granularity = $rate_detail->sec_granularity;
 
                       # length($cdr->billsec) ? $cdr->billsec : $cdr->duration;
-          my $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
+          $seconds = $use_duration ? $cdr->duration : $cdr->billsec;
 
           $seconds += $granularity - ( $seconds % $granularity )
             if $seconds      # don't granular-ize 0 billsec calls (bills them)
@@ -561,7 +564,7 @@ sub calc_usage {
                                  'minutes'        => $minutes,
                                  'charge'         => $charge,
                                  'pretty_dst'     => $pretty_destnum,
-                                 'dst_regionname' => $rate_region->regionname,
+                                 'dst_regionname' => $regionname,
                                )
           );
 
@@ -577,11 +580,25 @@ sub calc_usage {
           #if ( $self->option('rating_method') eq 'upstream_simple' ) {
           if ( scalar(@call_details) == 1 ) {
             $call_details =
-              [ 'C', $call_details[0], $charge, $classnum, $phonenum ];
+              [ 'C',
+                $call_details[0],
+                $charge,
+                $classnum,
+                $phonenum,
+                $seconds,
+                $regionname,
+              ];
           } else { #only used for $rating_method eq 'upstream' now
             $csv->combine(@call_details);
             $call_details =
-              [ 'C', $csv->string, $charge, $classnum, $phonenum ];
+              [ 'C',
+                $csv->string,
+                $charge,
+                $classnum,
+                $phonenum,
+                $seconds,
+                $regionname,
+              ];
           }
           warn "  adding details on charge to invoice: [ ".
               join(', ', @{$call_details} ). " ]"
index 93a32df..8ecd416 100644 (file)
@@ -97,7 +97,9 @@ sub check {
 
   my $error = 
     $self->ut_numbern('classnum')
+    || $self->ut_numbern('weight')
     || $self->ut_text('classname')
+    || $self->ut_textn('format')
     || $self->ut_enum('disabled', [ '', 'Y' ])
   ;
   return $error if $error;
@@ -105,6 +107,269 @@ sub check {
   $self->SUPER::check;
 }
 
+=item summary_formats_labelhash
+
+Returns a list of line item format descriptions suitable for assigning to
+a hash. 
+
+=cut
+
+# transform hashes of arrays to arrays of hashes for false laziness removal?
+my %summary_formats = (
+  'simple' => { 
+    'label' => [ qw( Description Calls Minutes Amount ) ],
+    'fields' => [
+                  sub { shift->{description} },
+                  sub { shift->{calls} },
+                  sub { sprintf( '%.1f', shift->{duration}/60 ) },
+                  sub { shift->{amount} },
+                ],
+    'align'  => [ qw( l r r r ) ],
+    'span'   => [ qw( 4 1 1 1 ) ],            # unitprices?
+    'width'  => [ qw( 8.2cm 2.5cm 1.4cm 1.6cm ) ],   # don't like this
+  },
+  'simpler' => { 
+    'label' =>  [ qw( Description Calls Amount ) ],
+    'fields' => [
+                  sub { shift->{description} },
+                  sub { shift->{calls} },
+                  sub { shift->{amount} },
+                ],
+    'align'  => [ qw( l r r ) ],
+    'span'   => [ qw( 5 1 1 ) ],
+    'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
+  },
+  'minimal' => { 
+    'label' => [ qw( Amount ) ],
+    'fields' => [
+                  sub { '' },
+                ],
+    'align'  => [ qw( r ) ],
+    'span'   => [ qw( 7 ) ],            # unitprices?
+    'width'  => [ qw( 13.8cm ) ],   # don't like this
+  },
+);
+
+sub summary_formats_labelhash {
+  map { $_ => join(',', @{$summary_formats{$_}{label}}) } keys %summary_formats;
+}
+
+=item header_generator FORMAT
+
+Returns a coderef used for generation of an invoice line item header for this
+usage_class. FORMAT is either html or latex
+
+=cut
+
+my %html_align = (
+  'c' => 'center',
+  'l' => 'left',
+  'r' => 'right',
+);
+
+sub _generator_defaults {
+  my ( $self, $format ) = ( shift, shift );
+  return ( $summary_formats{$self->format}, ' ', ' ', ' ', sub { shift } );
+}
+
+sub header_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    $self->_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;  
+  };
+
+}
+
+=item description_generator FORMAT
+
+Returns a coderef used for generation of invoice line items for this
+usage_class.  FORMAT is either html or latex
+
+=cut
+
+sub description_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    $self->_generator_defaults($format);
+
+  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}}}";
+          };
+  }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>!;
+      };
+  }
+
+  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;
+  };
+
+}
+
+=item total_generator FORMAT
+
+Returns a coderef used for generation of invoice total lines for this
+usage_class.  FORMAT is either html or latex
+
+=cut
+
+sub total_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+#  $OUT .= '\FStotaldesc{' . $section->{'description'} . ' Total}' .
+#          '{' . $section->{'subtotal'} . '}' . "\n";
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    $self->_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
+
+# not used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub total_line_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+#     $OUT .= '\FStotaldesc{' . $line->{'total_item'} . '}' .
+#             '{' . $line->{'total_amount'} . '}' . "\n";
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    $self->_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 _populate_initial_data {
   my ($class, %opts) = @_;
 
index b7f7b97..df4674b 100644 (file)
 
           $OUT .=
             '<table class="invoice_longtable" CELLSPACING=0 WIDTH="100%">'.
-            '<tr>'.
-              '<th align="center">Ref</th>'.
-              '<th align="left">Description</th>'.
-              ( $unitprices 
-                  ? '<th align="left">Unit Price</th>'.
-                    '<th align="left">Quantity</th>'
-                  : ''
-                ).
-              '<th align="right">Amount</th>'.
+            '<tr>';
+
+          if ($section->{header_generator}) {
+            $OUT .= &{$section->{header_generator}}();
+          } else {
+            $OUT .=  '<th align="center">Ref</th>'.
+                     '<th align="left">Description</th>'.
+                        ( $unitprices 
+                          ? '<th align="left">Unit Price</th>'.
+                            '<th align="left">Quantity</th>'
+                          : ''
+                        ). 
+                      '<th align="right">Amount</th>';
+          }
             '</tr>';
 
           my $lastref = 0;
             @detail_items )
           {
             $OUT .=
-              '<tr class="invoice_desc'.
-                ( ($line->{'ref'} && $line->{'ref'} ne $lastref) ? '' : '_more' ).
-                '">'.
-                '<td align="center">'. 
-                ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
-                '<td align="left">'. $line->{'description'}. '</td>'.
-                ( $unitprices 
-                    ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
-                      '<td align="left">'. $line->{'quantity'}. '</td>'
-                    : ''
-                ).
-
-                '<td align="right">'. $line->{'amount'}. '</td>'.
-              '</tr>'
-            ;
+              '<tr class="invoice_desc';
+            if ( $section->{description_generator} ) {
+              $OUT .= &{$section->{description_generator}}($line);
+            } else {
+              $OUT .=  ( ($line->{'ref'} && $line->{'ref'} ne $lastref) ? '' : '_more' ).
+                       '">'.
+                       '<td align="center">'. 
+                       ( $line->{'ref'} ne $lastref ? $line->{'ref'} : '' ). '</td>'.
+                       '<td align="left">'. $line->{'description'}. '</td>'.
+                       ( $unitprices 
+                           ? '<td align="left">'. $line->{'unit_amount'}. '</td>'.
+                             '<td align="left">'. $line->{'quantity'}. '</td>'
+                           : ''
+                       ).
+
+                       '<td align="right">'. $line->{'amount'}. '</td>';
+            }
+            $OUT .= '</tr>';
             $lastref = $line->{'ref'};
             if ( @{$line->{'ext_description'} } ) {
               $OUT .= '<tr class="invoice_extdesc"><td></td><td';
                         'border-bottom: 3px solid #000000;';
             $OUT .=
               '<tr class="invoice_totaldesc">'.
-                qq(<td style="$style">&nbsp;</td>).
-                qq(<td align="left" style="$style"). 
-                  ( $unitprices ? ' colspan=3>' : '>' ).
-                  $section->{'description'}. ' Total </td>'.
-                qq(<td align="right" style="$style">).
-                  $section->{'subtotal'}. '</td>'.
-              '</tr>'
-          ;
+              qq(<td style="$style">&nbsp;</td>);
+            if ($section->{total_generator}) {
+              $OUT .= &{$section->{total_generator}}($section);
+            } else {
+              $OUT .= qq(<td align="left" style="$style"). 
+                      ( $unitprices ? ' colspan=3>' : '>' ).
+                      $section->{'description'}. ' Total </td>'.
+                      qq(<td align="right" style="$style">).
+                      $section->{'subtotal'}. '</td>';
+            }
+            $OUT .= '</tr>';
           }
         } 
         if ($section->{'posttotal'}) {
           if ++$linenum == scalar(@total_items);
 
         $OUT .=
-          '<tr class="invoice_totaldesc">'.
-            qq(<td style="$style">&nbsp;</td>).
-            qq(<td align="left" style="$style").
-            ( $unitprices ? ' colspan=3>' : '>' ).
-              $line->{'total_item'}. '</td>'.
-            qq(<td align="right" style="$style">).
-              $line->{'total_amount'}. '</td>'.
-          '</tr>'
-        ;
+          '<tr class="invoice_totaldesc">';
+        if ($section->{total_line_generator}) {
+          $OUT .= &{$section->{total_line_generator}}($line);
+        } else {
+          $OUT .= qq(<td style="$style">&nbsp;</td>).
+                  qq(<td align="left" style="$style").
+                  ( $unitprices ? ' colspan=3>' : '>' ).
+                    $line->{'total_item'}. '</td>'.
+                  qq(<td align="right" style="$style">).
+                    $line->{'total_amount'}. '</td>';
+        }
+        $OUT .= '</tr>';
 
         $style='';
 
index a156c06..16da230 100644 (file)
@@ -3,15 +3,15 @@
                  'html_init'   => $html_init,
                  'name'        => 'package categories',
                  'disableable' => 1,
-                 'disabled_statuspos' => 2,
+                 'disabled_statuspos' => 3,
                  'query'       => { 'table'     => 'pkg_category',
                                     'hashref'   => {},
                                     'extra_sql' => 'ORDER BY categorynum',
                                   },
                  'count_query' => $count_query,
-                 'header'      => [ '#', 'Category', 'Weight' ],
-                 'fields'      => [ 'categorynum', 'categoryname', 'weight' ],
-                 'links'       => [ $link, $link, $link ],
+                 'header'      => [ '#', 'Category', 'Weight', 'Condense' ],
+                 'fields'      => [ 'categorynum', 'categoryname', 'weight', 'condense' ],
+                 'links'       => [ $link, $link, $link, $link ],
              )
 %>
 
index 63fd2c5..75223e0 100644 (file)
@@ -9,9 +9,21 @@
                                     'extra_sql' => 'ORDER BY classnum',
                                   },
                  'count_query' => 'SELECT COUNT(*) FROM usage_class',
-                 'header'      => [ '#', 'Class' ],
-                 'fields'      => [ 'classnum', 'classname' ],
-                 'links'       => [ $link, $link ],
+                 'header'      => [ '#',
+                                    'Class',
+                                    'Weight',
+                                    ( $useformat ? ('Format') : () ),
+                                  ],
+                 'fields'      => [ 'classnum',
+                                    'classname',
+                                    'weight',
+                                    ( $useformat ? (sub { $labels->{shift->format} } ) : () ),
+                                  ],
+                 'links'       => [ $link,
+                                    $link,
+                                    $link,
+                                    ( $useformat ? ( $link ) : () ),
+                                  ],
              )
 %>
 <%init>
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
+my $conf = new FS::Conf;
+my $useformat = $conf->exists('usage_class_as_a_section');
+my $labels = { &FS::usage_class::summary_formats_labelhash() };
+
+
 my $html_init = 
   'Usage classes define groups of usage for taxation purposes.<BR><BR>'.
   qq!<A HREF="${p}edit/usage_class.html"><I>Add a usage class</I></A><BR><BR>!;
index a244bd5..20e1093 100644 (file)
@@ -1,5 +1,28 @@
-<% include( 'elements/category_Common.html',
+<% include( 'elements/edit.html',
               'name'   => 'Package Category',
               'table'  => 'pkg_category',
-          )
+              'fields' => [
+                            'categoryname',
+                            'weight',
+                            { field=>'condense', type=>'checkbox', value=>'Y', },
+                            { field=>'disabled', type=>'checkbox', value=>'Y', },
+                          ],
+              'labels' => {
+                            'categorynum'  => 'Category number',
+                            'categoryname' => 'Category name',
+                            'weight'       => 'Weight',
+                            'condense'     => 'Collapse identical items to one',
+                            'disabled'     => 'Disable category',
+                          },
+              'viewall_dir' => 'browse',
+              %opt,
+           )
 %>
+<%init>
+
+die "access denied"
+  unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
+
+my %opt = @_;
+
+</%init>
index ef4b1ff..be01d2e 100644 (file)
@@ -3,14 +3,26 @@
               'table'         => 'usage_class',
               'fields'        => [
                                    'classname',
-                                   { field=>'disabled',
-                                     type=>'checkbox',
-                                     value=>'Y',
+                                   'weight',
+                                   { field => 'format',
+                                     type  => $useformat ? 'select' : 'hidden',
+                                     ( $useformat
+                                       ? ( 'options' => [ keys %labels ],
+                                           'labels'  => \%labels,
+                                         )
+                                       : ()
+                                     ),
+                                   },
+                                   { field => 'disabled',
+                                     type  => 'checkbox',
+                                     value => 'Y',
                                    },
                                  ],
               'labels'        => { 
                                    'classnum'  => 'Class number',
                                    'classname' => 'Class name',
+                                   'weight'    => 'Weight',
+                                   'format'    => 'Format',
                                    'disabled'  => 'Disable class',
                                  },
               'viewall_dir'   => 'browse',
@@ -22,4 +34,9 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Configuration');
 
+my $conf = new FS::Conf;
+my $useformat = $conf->exists('usage_class_as_a_section');
+
+my %labels = &FS::usage_class::summary_formats_labelhash();
+
 </%init>