[freeside-commits] branch FREESIDE_3_BRANCH updated. e1c6b4af716fecad943bf282b50c0d459b986720

Mark Wells mark at 420.am
Thu Feb 27 15:08:42 PST 2014


The branch, FREESIDE_3_BRANCH has been updated
       via  e1c6b4af716fecad943bf282b50c0d459b986720 (commit)
      from  6767c91fd38ea1d8e609e57f6c46c2b9da233f70 (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit e1c6b4af716fecad943bf282b50c0d459b986720
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Feb 27 13:06:56 2014 -0800

    package fees and usage-based fees, #27687, #25899

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index f160d3f..e4e4117 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -356,6 +356,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::part_fee;
   use FS::cust_bill_pkg_fee;
   use FS::part_fee_msgcat;
+  use FS::part_fee_usage;
   # Sammath Naur
 
   if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index 1e060c7..b832161 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -2195,6 +2195,18 @@ sub tables_hashref {
       'index'        => [],
     },
 
+    'part_fee_usage' => {
+      'columns' => [
+        'feepartusagenum','serial',     '',        '', '', '',
+        'feepart',           'int',     '',        '', '', '',
+        'classnum',          'int',     '',        '', '', '',
+        'amount',   @money_type,                '', '',
+        'percent',     'decimal',    '', '7,4', '', '',
+      ],
+      'primary_key'  => 'feepartusagenum',
+      'unique'       => [ [ 'feepart', 'classnum' ] ],
+      'index'        => [],
+    },
 
     'part_pkg_link' => {
       'columns' => [
diff --git a/FS/FS/cust_event_fee.pm b/FS/FS/cust_event_fee.pm
index 29d7c5c..ad78b0e 100644
--- a/FS/FS/cust_event_fee.pm
+++ b/FS/FS/cust_event_fee.pm
@@ -114,7 +114,8 @@ sub check {
 =item by_cust CUSTNUM[, PARAMS]
 
 Finds all cust_event_fee records belonging to the customer CUSTNUM.  Currently
-fee events can be cust_main or cust_bill events; this will return both.
+fee events can be cust_main, cust_pkg, or cust_bill events; this will return 
+all of them.
 
 PARAMS can be additional params to pass to qsearch; this really only works
 for 'hashref' and 'order_by'.
@@ -147,6 +148,15 @@ sub by_cust {
     extra_sql => "$where eventtable = 'cust_bill' ".
                  "AND cust_bill.custnum = $custnum",
     %params
+  }),
+  qsearch({
+    table     => 'cust_event_fee',
+    addl_from => 'JOIN cust_event USING (eventnum) ' .
+                 'JOIN part_event USING (eventpart) ' .
+                 'JOIN cust_pkg ON (cust_event.tablenum = cust_pkg.pkgnum)',
+    extra_sql => "$where eventtable = 'cust_pkg' ".
+                 "AND cust_pkg.custnum = $custnum",
+    %params
   })
 }
 
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 5ce94fe..a15c990 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -564,7 +564,7 @@ sub bill {
       my $object = $event_fee->cust_event->cust_X;
       my $part_fee = $event_fee->part_fee;
       my $cust_bill;
-      if ( $object->isa('FS::cust_main') ) {
+      if ( $object->isa('FS::cust_main') or $object->isa('FS::cust_pkg') ) {
         # Not the real cust_bill object that will be inserted--in particular
         # there are no taxes yet.  If you want to charge a fee on the total 
         # invoice amount including taxes, you have to put the fee on the next
@@ -575,6 +575,15 @@ sub bill {
             'charged'       => ${ $total_setup{$pass} } +
                                ${ $total_recur{$pass} },
         });
+
+        # If this is a package event, only apply the fee to line items 
+        # from that package.
+        if ($object->isa('FS::cust_pkg')) {
+          $cust_bill->set('cust_bill_pkg', 
+            [ grep  { $_->pkgnum == $object->pkgnum } @cust_bill_pkg ]
+          );
+        }
+
       } elsif ( $object->isa('FS::cust_bill') ) {
         # simple case: applying the fee to a previous invoice (late fee, 
         # etc.)
@@ -591,7 +600,7 @@ sub bill {
       # also skip if it's disabled
       next if $part_fee->disabled eq 'Y';
       # calculate the fee
-      my $fee_item = $event_fee->part_fee->lineitem($cust_bill);
+      my $fee_item = $part_fee->lineitem($cust_bill) or next;
       # link this so that we can clear the marker on inserting the line item
       $fee_item->set('cust_event_fee', $event_fee);
       push @fee_items, $fee_item;
diff --git a/FS/FS/part_event/Action/pkg_fee.pm b/FS/FS/part_event/Action/pkg_fee.pm
new file mode 100644
index 0000000..7e409a5
--- /dev/null
+++ b/FS/FS/part_event/Action/pkg_fee.pm
@@ -0,0 +1,16 @@
+package FS::part_event::Action::pkg_fee;
+
+use strict;
+use base qw( FS::part_event::Action::Mixin::fee );
+
+sub description { 'Charge a fee when this package is billed'; }
+
+sub eventtable_hashref {
+    { 'cust_pkg' => 1 };
+}
+
+sub hold_until_bill { 1 }
+
+# Functionally identical to cust_fee.
+
+1;
diff --git a/FS/FS/part_fee.pm b/FS/FS/part_fee.pm
index cd895eb..fe63250 100644
--- a/FS/FS/part_fee.pm
+++ b/FS/FS/part_fee.pm
@@ -7,6 +7,7 @@ use FS::Record qw( qsearch qsearchs );
 use FS::pkg_class;
 use FS::part_pkg_taxproduct;
 use FS::agent;
+use FS::part_fee_usage;
 
 $DEBUG = 0;
 
@@ -149,23 +150,20 @@ sub check {
     || $self->ut_moneyn('minimum')
     || $self->ut_moneyn('maximum')
     || $self->ut_flag('limit_credit')
-    || $self->ut_enum('basis', [ '', 'charged', 'owed' ])
+    || $self->ut_enum('basis', [ 'charged', 'owed', 'usage' ])
     || $self->ut_enum('setuprecur', [ 'setup', 'recur' ])
   ;
   return $error if $error;
 
-  return "For a percentage fee, the basis must be set"
-    if $self->get('percent') > 0 and $self->get('basis') eq '';
-
-  if ( ! $self->get('percent') and ! $self->get('limit_credit') ) {
-    # then it makes no sense to apply minimum/maximum
-    $self->set('minimum', '');
-    $self->set('maximum', '');
-  }
   if ( $self->get('limit_credit') ) {
     $self->set('maximum', '');
   }
 
+  if ( $self->get('basis') eq 'usage' ) {
+    # to avoid confusion, don't also allow charging a percentage
+    $self->set('percent', 0);
+  }
+
   $self->SUPER::check;
 }
 
@@ -181,7 +179,7 @@ sub explanation {
   my $money_char = FS::Conf->new->config('money_char') || '$';
   my $money = $money_char . '%.2f';
   my $percent = '%.1f%%';
-  my $string;
+  my $string = '';
   if ( $self->amount > 0 ) {
     $string = sprintf($money, $self->amount);
   }
@@ -196,7 +194,14 @@ sub explanation {
     } elsif ( $self->basis('owed') ) {
       $string .= 'unpaid invoice balance';
     }
+  } elsif ( $self->basis eq 'usage' ) {
+    if ( $string ) {
+      $string .= " plus \n";
+    }
+    # append per-class descriptions
+    $string .= join("\n", map { $_->explanation } $self->part_fee_usage);
   }
+
   if ( $self->minimum or $self->maximum or $self->limit_credit ) {
     $string .= "\nbut";
     if ( $self->minimum ) {
@@ -247,37 +252,68 @@ sub lineitem {
   warn "Calculating fee: ".$self->itemdesc." on ".
     ($cust_bill->invnum ? "invoice #".$cust_bill->invnum : "current invoice").
     "\n" if $DEBUG;
-  if ( $self->percent > 0 and $self->basis ne '' ) {
-    warn $self->percent . "% of amount ".$self->basis.")\n"
-      if $DEBUG;
-
-    # $total_base: the total charged/owed on the invoice
-    # %item_base: billpkgnum => fraction of base amount
-    if ( $cust_bill->invnum ) {
-      my $basis = $self->basis;
-      $total_base = $cust_bill->$basis; # "charged", "owed"
+  my $basis = $self->basis;
+
+  # $total_base: the total charged/owed on the invoice
+  # %item_base: billpkgnum => fraction of base amount
+  if ( $cust_bill->invnum ) {
 
-      # calculate the fee on an already-inserted past invoice.  This may have 
-      # payments or credits, so if basis = owed, we need to consider those.
+    # calculate the fee on an already-inserted past invoice.  This may have 
+    # payments or credits, so if basis = owed, we need to consider those.
+    @items = $cust_bill->cust_bill_pkg;
+    if ( $basis ne 'usage' ) {
+
+      $total_base = $cust_bill->$basis; # "charged", "owed"
       my $basis_sql = $basis.'_sql';
       my $sql = 'SELECT ' . FS::cust_bill_pkg->$basis_sql .
                 ' FROM cust_bill_pkg WHERE billpkgnum = ?';
-      @items = $cust_bill->cust_bill_pkg;
       @item_base = map { FS::Record->scalar_sql($sql, $_->billpkgnum) }
                     @items;
-    } else {
-      # the fee applies to _this_ invoice.  It has no payments or credits, so
-      # "charged" and "owed" basis are both just the invoice amount, and 
-      # the line item amounts (setup + recur)
+    }
+  } else {
+    # the fee applies to _this_ invoice.  It has no payments or credits, so
+    # "charged" and "owed" basis are both just the invoice amount, and 
+    # the line item amounts (setup + recur)
+    @items = @{ $cust_bill->get('cust_bill_pkg') };
+    if ( $basis ne 'usage' ) {
       $total_base = $cust_bill->charged;
-      @items = @{ $cust_bill->get('cust_bill_pkg') };
       @item_base = map { $_->setup + $_->recur }
                     @items;
     }
-
-    $amount += $total_base * $self->percent / 100;
   }
 
+  if ( $basis eq 'usage' ) {
+
+    my %part_fee_usage = map { $_->classnum => $_ } $self->part_fee_usage;
+
+    foreach my $item (@items) { # cust_bill_pkg objects
+      my $usage_fee = 0;
+      $item->regularize_details;
+      my $details;
+      if ( $item->billpkgnum ) {
+        $details = [
+          qsearch('cust_bill_pkg_detail', { billpkgnum => $item->billpkgnum })
+        ];
+      } else {
+        $details = $item->get('details') || [];
+      }
+      foreach my $d (@$details) {
+        # if there's a usage fee defined for this class...
+        next if $d->amount eq '' # not a real usage detail
+             or $d->amount == 0  # zero charge, probably shouldn't charge fee
+        ;
+        my $p = $part_fee_usage{$d->classnum} or next;
+        $usage_fee += ($d->amount * $p->percent / 100)
+                    + $p->amount;
+        # we'd create detail records here if we were doing that
+      }
+      # bypass @item_base entirely
+      push @item_fee, $usage_fee;
+      $amount += $usage_fee;
+    }
+
+  } # if $basis eq 'usage'
+
   if ( $self->minimum ne '' and $amount < $self->minimum ) {
     warn "Applying mininum fee\n" if $DEBUG;
     $amount = $self->minimum;
@@ -368,25 +404,25 @@ sub lineitem {
         }
       }
     }
-    # and add them to the cust_bill_pkg
+  }
+  if ( @item_fee ) {
+    # add allocation records to the cust_bill_pkg
     for (my $i = 0; $i < scalar(@items); $i++) {
       if ( $item_fee[$i] > 0 ) {
         push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
             cust_bill_pkg   => $cust_bill_pkg,
-            base_invnum     => $cust_bill->invnum,
+            base_invnum     => $cust_bill->invnum, # may be null
             amount          => $item_fee[$i],
             base_cust_bill_pkg => $items[$i], # late resolve
         });
       }
     }
-  } else { # if !@item_base
+  } else { # if !@item_fee
     # then this isn't a proportional fee, so it just applies to the 
     # entire invoice.
-    # (if it's the current invoice, $cust_bill->invnum is null and that 
-    # will be fixed later)
     push @cust_bill_pkg_fee, FS::cust_bill_pkg_fee->new({
         cust_bill_pkg   => $cust_bill_pkg,
-        base_invnum     => $cust_bill->invnum,
+        base_invnum     => $cust_bill->invnum, # may be null
         amount          => $amount,
     });
   }
@@ -483,6 +519,11 @@ sub part_fee_msgcat {
   qsearch( 'part_fee_msgcat', { feepart => $self->feepart } );
 }
 
+sub part_fee_usage {
+  my $self = shift;
+  qsearch( 'part_fee_usage', { feepart => $self->feepart } );
+}
+
 =back
 
 =head1 BUGS
diff --git a/FS/FS/part_fee_usage.pm b/FS/FS/part_fee_usage.pm
new file mode 100644
index 0000000..701c8c5
--- /dev/null
+++ b/FS/FS/part_fee_usage.pm
@@ -0,0 +1,144 @@
+package FS::part_fee_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+use FS::part_fee;
+use FS::usage_class;
+use FS::Conf;
+
+=head1 NAME
+
+FS::part_fee_usage - Object methods for part_fee_usage records
+
+=head1 SYNOPSIS
+
+  use FS::part_fee_usage;
+
+  $record = new FS::part_fee_usage \%hash;
+  $record = new FS::part_fee_usage { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_fee_usage object is the part of a processing fee definition 
+(L<FS::part_fee>) that applies to a specific telephone usage class 
+(L<FS::usage_class>).  FS::part_fee_usage inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item feepartusagenum - primary key
+
+=item feepart - foreign key to L<FS::part_pkg>
+
+=item classnum - foreign key to L<FS::usage_class>
+
+=item amount - fixed amount to charge per usage record
+
+=item percent - percentage of rated price to charge per usage record
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=cut
+
+sub table { 'part_fee_usage'; }
+
+sub check {
+  my $self = shift;
+
+  $self->set('amount', 0)  unless ($self->amount || 0) > 0;
+  $self->set('percent', 0) unless ($self->percent || 0) > 0;
+
+  my $error = 
+    $self->ut_numbern('feepartusagenum')
+    || $self->ut_foreign_key('feepart', 'part_fee', 'feepart')
+    || $self->ut_foreign_key('classnum', 'usage_class', 'classnum')
+    || $self->ut_money('amount')
+    || $self->ut_float('percent')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+# silently discard records with percent = 0 and amount = 0
+
+sub insert {
+  my $self = shift;
+  if ( $self->amount > 0 or $self->percent > 0 ) {
+    return $self->SUPER::insert;
+  }
+  '';
+}
+
+sub replace {
+  my ($new, $old) = @_;
+  $old ||= $new->replace_old;
+  if ( $new->amount > 0 or $new->percent > 0 ) {
+    return $new->SUPER::replace($old);
+  } elsif ( $old->feepartusagenum ) {
+    return $old->delete;
+  }
+  '';
+}
+  
+=item explanation
+
+Returns a string describing how this fee is calculated.
+
+=cut
+
+sub explanation {
+  my $self = shift;
+  my $string = '';
+  my $money = (FS::Conf->new->config('money_char') || '$') . '%.2f';
+  my $percent = '%.1f%%';
+  if ( $self->amount > 0 ) {
+    $string = sprintf($money, $self->amount);
+  }
+  if ( $self->percent > 0 ) {
+    if ( $string ) {
+      $string .= ' plus ';
+    }
+    $string .= sprintf($percent, $self->percent);
+    $string .= ' of the rated charge';
+  }
+  $string .= ' per '.  $self->usage_class->classname . ' call';
+
+  return $string;
+}
+
+# stubs, remove under 4.x
+
+sub part_fee {
+  my $self = shift;
+  FS::part_fee->by_key($self->feepart);
+}
+
+sub usage_class {
+  my $self = shift;
+  FS::usage_class->by_key($self->classnum);
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 1cc68cc..c453f87 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -726,3 +726,5 @@ t/cust_bill_pkg_fee.t
 FS/part_fee_msgcat.pm
 t/part_fee_msgcat.t
 
+FS/part_fee_usage.pm
+t/part_fee_usage.t
diff --git a/FS/t/part_fee_usage.t b/FS/t/part_fee_usage.t
new file mode 100644
index 0000000..cb7fb22
--- /dev/null
+++ b/FS/t/part_fee_usage.t
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::part_fee_usage;
+$loaded=1;
+print "ok 1\n";
diff --git a/httemplate/edit/part_fee.html b/httemplate/edit/part_fee.html
index dada233..e057a75 100644
--- a/httemplate/edit/part_fee.html
+++ b/httemplate/edit/part_fee.html
@@ -14,7 +14,6 @@
     'credit_weight' => 'Credit weight',
     'agentnum'      => 'Agent',
     'amount'        => 'Flat fee amount',
-    'percent'       => 'Percentage of invoice amount',
     'basis'         => 'Based on',
     'setuprecur'    => 'Report this fee as',
     'minimum'       => 'Minimum fee',
@@ -55,8 +54,8 @@ my $n = 0;
 my (@locale_fields, %locale_labels);
 foreach (@locales) {
   push @locale_fields,
-    { field => 'feepartmsgnum'. $n,               type => 'hidden' },
-    { field => 'feepartmsgnum'. $n. '_locale',    type => 'hidden' },
+    { field => 'feepartmsgnum'. $n,            type => 'hidden' },
+    { field => 'feepartmsgnum'. $n. '_locale', type => 'hidden', value => $_ },
     { field => 'feepartmsgnum'. $n. '_itemdesc',  type => 'text', size => 40 },
   ;
   $locale_labels{ 'feepartmsgnum'.$n.'_itemdesc' } =
@@ -64,6 +63,19 @@ foreach (@locales) {
   $n++;
 }
 
+$n = 0;
+my %layer_fields = (
+  'charged' => [
+    'percent' => { label => 'Fraction of invoice total', type    => 'percentage', },
+  ],
+  'owed' => [
+    'percent' => { label => 'Fraction of balance', type    => 'percentage', },
+  ],
+  'usage' => [
+    'usage'   => { type => 'part_fee_usage' }
+  ],
+);
+
 my @fields = (
 
   { field   => 'itemdesc',  type    => 'text', size    => 40, },
@@ -95,15 +107,23 @@ my @fields = (
 
   { type => 'justtitle', value => 'Fee calculation' },
   { field   => 'amount',  type    => 'money', },
-  { field   => 'percent', type    => 'percentage', },
 
   { field   => 'basis',
-    type    => 'select',
-    options => [ 'charged', 'owed' ],
-    labels  => { 'charged' => 'amount charged',
-                 'owed'    => 'balance due', },
+    type    => 'selectlayers',
+    options => [ 'charged', 'owed', 'usage' ],
+    labels  => { 'charged'  => 'amount charged',
+                 'owed'     => 'balance due',
+                 'usage'    => 'usage charges' },
+    layer_fields => \%layer_fields,
+    layer_values_callback => sub {
+      my ($cgi, $obj) = @_;
+      {
+        'charged' => { percent => $obj->percent },
+        'owed'    => { percent => $obj->percent },
+        'usage'   => { usage => [ $obj->part_fee_usage ] },
+      }
+    },
   },
-
   { field   => 'minimum', type    => 'money', },
   { field   => 'maximum', type    => 'money', },
   { field   => 'limit_credit',
diff --git a/httemplate/edit/process/part_fee.html b/httemplate/edit/process/part_fee.html
index 25656e9..6d17863 100644
--- a/httemplate/edit/process/part_fee.html
+++ b/httemplate/edit/process/part_fee.html
@@ -4,10 +4,19 @@
   'agent_virt'        => 1,
   'agent_null_right'  => 'Edit global fee definitions',
   'viewall_dir'       => 'browse',
-  'process_o2m'       => {
-                            'table'   => 'part_fee_msgcat',
-                            'fields'  => [ 'locale', 'itemdesc' ],
-                         },
+  'process_o2m'       => [ 
+                           {
+                              'table'   => 'part_fee_msgcat',
+                              'fields'  => [ 'locale', 'itemdesc' ],
+                           },
+                           {
+                              'table'   => 'part_fee_usage',
+                              'fields'  => [ 'classnum', 
+                                             'amount',
+                                             'percent'
+                                           ],
+                           },
+                         ],
 &>
 <%init>
 
diff --git a/httemplate/elements/tr-part_fee_usage.html b/httemplate/elements/tr-part_fee_usage.html
new file mode 100644
index 0000000..00f4e12
--- /dev/null
+++ b/httemplate/elements/tr-part_fee_usage.html
@@ -0,0 +1,29 @@
+% my $n = 0;
+% foreach my $class (@classes) {
+%   my $pre = "feepartusagenum$n";
+%   my $x = $part_fee_usage{$class->classnum} || FS::part_fee_usage->new({});
+<tr>
+  <td align="right">
+    <input type="hidden" name="<%$pre%>" value="<% $x->partfeeusagenum %>">
+    <input type="hidden" name="<%$pre%>_classnum" value="<% $class->classnum %>">
+    <% $class->classname %>:</td>
+  <td>
+    <%$money_char%><input size=4 name="<%$pre%>_amount" \
+    value="<% sprintf("%.2f", $x->amount) %>">
+  </td>
+  <td>per call<b> + </b></td>
+  <td>
+    <input size=4 name="<%$pre%>_percent" \
+    value="<% sprintf("%.1f", $x->percent) %>">%
+  </td>
+</tr>
+%   $n++;
+% }
+<%init>
+my %opt = @_;
+my $value = $opt{'curr_value'} || $opt{'value'};
+# values is an arrayref of part_fee_usage objects
+my %part_fee_usage = map { $_->classnum => $_ } @$value;
+my @classes = qsearch('usage_class', { disabled => '' });
+my $money_char = FS::Conf->new->config('money_char') || '$';
+</%init>

-----------------------------------------------------------------------

Summary of changes:
 FS/FS/Mason.pm                             |    1 +
 FS/FS/Schema.pm                            |   12 +++
 FS/FS/cust_event_fee.pm                    |   12 ++-
 FS/FS/cust_main/Billing.pm                 |   13 ++-
 FS/FS/part_event/Action/pkg_fee.pm         |   16 +++
 FS/FS/part_fee.pm                          |  111 +++++++++++++++-------
 FS/FS/part_fee_usage.pm                    |  144 ++++++++++++++++++++++++++++
 FS/MANIFEST                                |    2 +
 FS/t/{AccessRight.t => part_fee_usage.t}   |    2 +-
 httemplate/edit/part_fee.html              |   38 ++++++--
 httemplate/edit/process/part_fee.html      |   17 +++-
 httemplate/elements/tr-part_fee_usage.html |   29 ++++++
 12 files changed, 345 insertions(+), 52 deletions(-)
 create mode 100644 FS/FS/part_event/Action/pkg_fee.pm
 create mode 100644 FS/FS/part_fee_usage.pm
 copy FS/t/{AccessRight.t => part_fee_usage.t} (80%)
 create mode 100644 httemplate/elements/tr-part_fee_usage.html




More information about the freeside-commits mailing list