[freeside-commits] branch master updated. a0e00fa0547e99893c735ab3dbdacdb2bb054f5a

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


The branch, master has been updated
       via  a0e00fa0547e99893c735ab3dbdacdb2bb054f5a (commit)
      from  55190e4a18ff318cf2a0ac2eb6abaf7a3b95e087 (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 a0e00fa0547e99893c735ab3dbdacdb2bb054f5a
Author: Mark Wells <mark at freeside.biz>
Date:   Thu Feb 27 14:04:52 2014 -0800

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

diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 7bf5446..cefdeaa 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -377,6 +377,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 795b97f..78cac4a 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3185,6 +3185,26 @@ sub tables_hashref {
                         ],
     },
 
+    'part_fee_usage' => {
+      'columns' => [
+        'feepartusagenum','serial',     '',        '', '', '',
+        'feepart',           'int',     '',        '', '', '',
+        'classnum',          'int',     '',        '', '', '',
+        'amount',   @money_type,                '', '',
+        'percent',     'decimal',    '', '7,4', '', '',
+      ],
+      'primary_key'  => 'feepartusagenum',
+      'unique'       => [ [ 'feepart', 'classnum' ] ],
+      'index'        => [],
+      'foreign_keys' => [
+                          { columns    => [ 'feepart' ],
+                            table      => 'part_fee',
+                          },
+                          { columns    => [ 'classnum' ],
+                            table      => 'pkg_class',
+                          },
+                        ],
+    },
 
     'part_pkg_link' => {
       'columns' => [
diff --git a/FS/FS/cust_event_fee.pm b/FS/FS/cust_event_fee.pm
index d924485..181640d 100644
--- a/FS/FS/cust_event_fee.pm
+++ b/FS/FS/cust_event_fee.pm
@@ -112,7 +112,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'.
@@ -145,6 +146,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 a7e7d19..8d38992 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 b0e5473..186fb34 100644
--- a/FS/FS/part_fee.pm
+++ b/FS/FS/part_fee.pm
@@ -146,23 +146,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;
 }
 
@@ -178,7 +175,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);
   }
@@ -193,7 +190,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 ) {
@@ -244,37 +248,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;
 
-      # calculate the fee on an already-inserted past invoice.  This may have 
-      # payments or credits, so if basis = owed, we need to consider those.
+  # $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.
+    @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;
@@ -365,25 +400,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,
     });
   }
diff --git a/FS/FS/part_fee_usage.pm b/FS/FS/part_fee_usage.pm
new file mode 100644
index 0000000..a1b85ae
--- /dev/null
+++ b/FS/FS/part_fee_usage.pm
@@ -0,0 +1,130 @@
+package FS::part_fee_usage;
+
+use strict;
+use base qw( FS::Record );
+use FS::Record qw( qsearch qsearchs );
+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;
+}
+
+=back
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/MANIFEST b/FS/MANIFEST
index 129ee64..7ba2226 100644
--- a/FS/MANIFEST
+++ b/FS/MANIFEST
@@ -766,3 +766,5 @@ FS/cust_bill_pkg_fee.pm
 t/cust_bill_pkg_fee.t
 FS/part_fee_msgcat.pm
 t/part_fee_msgcat.t
+FS/part_fee_usage.pm
+FS/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 b1044c9..e057a75 100644
--- a/httemplate/edit/part_fee.html
+++ b/httemplate/edit/part_fee.html
@@ -14,13 +14,11 @@
     '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',
     'maximum'       => 'Maximum fee',
     'limit_credit'  => 'Limit to customer credit balance',
-    'nextbill'      => 'Hold until the customer\'s next invoice',
     %locale_labels
   },
   'fields'        => \@fields,
@@ -56,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' } =
@@ -65,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, },
@@ -87,11 +98,6 @@ my @fields = (
     value   => 'Y',
   },
 
-  { field   => 'nextbill',
-    type    => 'checkbox',
-    value   => 'Y',
-  },
-
   { field   => 'setuprecur',
     type    => 'select',
     options => [ 'setup', 'recur' ],
@@ -101,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 100755
--- 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                            |   20 ++++
 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                          |  105 +++++++++++++++--------
 FS/FS/part_fee_usage.pm                    |  130 ++++++++++++++++++++++++++++
 FS/MANIFEST                                |    2 +
 FS/t/{AccessRight.t => part_fee_usage.t}   |    2 +-
 httemplate/edit/part_fee.html              |   44 ++++++---
 httemplate/edit/process/part_fee.html      |   17 +++-
 httemplate/elements/tr-part_fee_usage.html |   29 ++++++
 12 files changed, 333 insertions(+), 58 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