commit 82ff5451c8babb72b4963f8d3bbb4b604a3b4675
Author: Moritz Bunkus <m.bunkus@linet.de>
Date:   Thu Feb 5 13:38:07 2015 +0100

    Wiederkehrende Rechnungen: Auftragswerts-Periodizität setzen können
    
    Noch keine Anpassung der eigentlichen Berechnung. Wohl aber
    Dokumentation von SL::DB::PeriodicInvoicesConfig.

diff --git a/SL/BackgroundJob/CreatePeriodicInvoices.pm b/SL/BackgroundJob/CreatePeriodicInvoices.pm
index d80ba4eeb..e782d821b 100644
--- a/SL/BackgroundJob/CreatePeriodicInvoices.pm
+++ b/SL/BackgroundJob/CreatePeriodicInvoices.pm
@@ -74,7 +74,7 @@ sub _log_msg {
 sub _generate_time_period_variables {
   my $config            = shift;
   my $period_start_date = shift;
-  my $period_end_date   = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_period_length)->subtract(days => 1);
+  my $period_end_date   = $period_start_date->clone->truncate(to => 'month')->add(months => $config->get_billing_period_length)->subtract(days => 1);
 
   my @month_names       = ('',
                            $::locale->text('January'), $::locale->text('February'), $::locale->text('March'),     $::locale->text('April'),   $::locale->text('May'),      $::locale->text('June'),
diff --git a/SL/Controller/FinancialControllingReport.pm b/SL/Controller/FinancialControllingReport.pm
index c10c09dbe..8ac846428 100644
--- a/SL/Controller/FinancialControllingReport.pm
+++ b/SL/Controller/FinancialControllingReport.pm
@@ -148,7 +148,7 @@ sub calculate_periodic_invoices_order_netamount {
     $cur_date->add(years => 1);
   }
 
-  return $num_years * $order->netamount * (12 / $order->periodic_invoices_config->get_period_length);
+  return $num_years * $order->netamount * (12 / $order->periodic_invoices_config->get_billing_period_length);
 }
 
 sub sum_items {
diff --git a/SL/Controller/FinancialOverview.pm b/SL/Controller/FinancialOverview.pm
index cd153d23f..6b2889932 100644
--- a/SL/Controller/FinancialOverview.pm
+++ b/SL/Controller/FinancialOverview.pm
@@ -148,7 +148,7 @@ sub calculate_one_periodic_invoice {
   return if $params{config}->start_date > $params{end_date};
 
   my $first_date = $params{config}->start_date->clone->set_year($self->year);
-  my $net        = $params{config}->order->netamount * (12 / $params{config}->get_period_length);
+  my $net        = $params{config}->order->netamount * (12 / $params{config}->get_billing_period_length);
   my $sord       = $self->data->{sales_orders};
 
   $sord->{months  }->[ $first_date->month   - 1 ] += $net;
diff --git a/SL/DB/MetaSetup/PeriodicInvoicesConfig.pm b/SL/DB/MetaSetup/PeriodicInvoicesConfig.pm
index eae8481d4..1aeb42fd7 100644
--- a/SL/DB/MetaSetup/PeriodicInvoicesConfig.pm
+++ b/SL/DB/MetaSetup/PeriodicInvoicesConfig.pm
@@ -17,7 +17,8 @@ __PACKAGE__->meta->columns(
   first_billing_date      => { type => 'date' },
   id                      => { type => 'integer', not_null => 1, sequence => 'id' },
   oe_id                   => { type => 'integer', not_null => 1 },
-  periodicity             => { type => 'varchar', length => 10, not_null => 1 },
+  order_value_periodicity => { type => 'varchar', length => 1, not_null => 1 },
+  periodicity             => { type => 'varchar', length => 1, not_null => 1 },
   print                   => { type => 'boolean', default => 'false' },
   printer_id              => { type => 'integer' },
   start_date              => { type => 'date' },
diff --git a/SL/DB/PeriodicInvoicesConfig.pm b/SL/DB/PeriodicInvoicesConfig.pm
index 8013cd940..53ac1c679 100644
--- a/SL/DB/PeriodicInvoicesConfig.pm
+++ b/SL/DB/PeriodicInvoicesConfig.pm
@@ -11,14 +11,22 @@ __PACKAGE__->meta->initialize;
 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
 __PACKAGE__->meta->make_manager_class;
 
-our @PERIODICITIES  = qw(m q f b y);
-our %PERIOD_LENGTHS = ( m => 1, q => 3, f => 4, b => 6, y => 12 );
+our %PERIOD_LENGTHS             = ( m => 1, q => 3, b => 6, y => 12 );
+our %ORDER_VALUE_PERIOD_LENGTHS = ( %PERIOD_LENGTHS, 2 => 24, 3 => 36, 4 => 48, 5 => 60 );
+our @PERIODICITIES              = keys %PERIOD_LENGTHS;
+our @ORDER_VALUE_PERIODICITIES  = keys %ORDER_VALUE_PERIOD_LENGTHS;
 
-sub get_period_length {
+sub get_billing_period_length {
   my $self = shift;
   return $PERIOD_LENGTHS{ $self->periodicity } || 1;
 }
 
+sub get_order_value_period_length {
+  my $self = shift;
+  return $self->get_billing_period_length if $self->order_value_periodicity eq 'p';
+  return $ORDER_VALUE_PERIOD_LENGTHS{ $self->order_value_periodicity } || 1;
+}
+
 sub _log_msg {
   $::lxdebug->message(LXDebug->DEBUG1(), join('', @_));
 }
@@ -78,7 +86,7 @@ SQL
 sub calculate_invoice_dates {
   my ($self, %params) = @_;
 
-  my $period_len = $self->get_period_length;
+  my $period_len = $self->get_billing_period_length;
   my $cur_date   = $self->first_billing_date || $self->start_date;
   my $end_date   = $self->terminated ? $self->end_date : undef;
   $end_date    //= DateTime->today_local->add(years => 100);
@@ -100,3 +108,98 @@ sub calculate_invoice_dates {
 }
 
 1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::PeriodicInvoicesConfig - DB model for the configuration for periodic invoices
+
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<calculate_invoice_dates %params>
+
+Calculates dates for which invoices will have to be created. Returns a
+list of L<DateTime> objects.
+
+This function looks at the configuration settings and at the list of
+invoices that have already been created for this configuration. The
+date range for which dates are created are controlled by several
+values:
+
+=over 2
+
+=item * The properties C<first_billing_date> and C<start_date>
+determine the start date.
+
+=item * The properties C<end_date> and C<terminated> determine the end
+date.
+
+=item * The optional parameter C<past_dates> determines whether or not
+dates for which invoices have already been created will be included in
+the list. The default is not to include them.
+
+=item * The optional parameters C<start_date> and C<end_date> override
+the start and end dates from the configuration.
+
+=item * If no end date is set or implied via the configuration and no
+C<end_date> parameter is given then the function will use 100 years
+in the future as the end date.
+
+=back
+
+=item C<get_billing_period_length>
+
+Returns the number of months corresponding to the billing
+periodicity. This means that a new invoice has to be created every x
+months starting with the value in C<first_billing_date> (or
+C<start_date> if C<first_billing_date> is unset).
+
+=item C<get_order_value_period_length>
+
+Returns the number of months the order's value refers to. This looks
+at the C<order_value_periodicity>.
+
+Each invoice's value is calculated as C<order value *
+billing_period_length / order_value_period_length>.
+
+=item C<get_previous_billed_period_start_date>
+
+Returns the highest date (as an instance of L<DateTime>) for which an
+invoice has been created from this configuration.
+
+=item C<handle_automatic_extension>
+
+Configurations which haven't been terminated and which have an end
+date set may be eligible for automatic extension by a certain number
+of months. This what the function implements.
+
+If the configuration is not eligible or if the C<end_date> hasn't been
+reached yet then nothing is done and C<undef> is returned. Otherwise
+its behavior is determined by the C<extend_automatically_by> property.
+
+If the property C<extend_automatically_by> is not 0 then the
+C<end_date> will be extended by C<extend_automatically_by> months, and
+the configuration will be saved. In this case the new end date will be
+returned.
+
+Otherwise (if C<extend_automatically_by> is 0) the property C<active>
+will be set to 1, and the configuration will be saved. In this case
+C<undef> will be returned.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
diff --git a/SL/OE.pm b/SL/OE.pm
index 061cd27a0..9944f71cd 100644
--- a/SL/OE.pm
+++ b/SL/OE.pm
@@ -738,7 +738,7 @@ sub load_periodic_invoice_config {
     my $config_obj = SL::DB::Manager::PeriodicInvoicesConfig->find_by(oe_id => $form->{id});
 
     if ($config_obj) {
-      my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity start_date_as_date end_date_as_date first_billing_date_as_date extend_automatically_by ar_chart_id
+      my $config = { map { $_ => $config_obj->$_ } qw(active terminated periodicity order_value_periodicity start_date_as_date end_date_as_date first_billing_date_as_date extend_automatically_by ar_chart_id
                                                       print printer_id copies) };
       $form->{periodic_invoices_config} = YAML::Dump($config);
     }
diff --git a/bin/mozilla/oe.pl b/bin/mozilla/oe.pl
index 4483deb1f..147e9d9de 100644
--- a/bin/mozilla/oe.pl
+++ b/bin/mozilla/oe.pl
@@ -2000,14 +2000,16 @@ sub edit_periodic_invoices_config {
   $config = YAML::Load($::form->{periodic_invoices_config}) if $::form->{periodic_invoices_config};
 
   if ('HASH' ne ref $config) {
-    $config =  { periodicity             => 'y',
+    $config =  { periodicity             => 'm',
+                 order_value_periodicity => 'p', # = same as periodicity
                  start_date_as_date      => $::form->{transdate} || $::form->current_date,
                  extend_automatically_by => 12,
                  active                  => 1,
                };
   }
 
-  $config->{periodicity} = 'm' if none { $_ eq $config->{periodicity} } qw(m q b y);
+  $config->{periodicity}             = 'm' if none { $_ eq $config->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES;
+  $config->{order_value_periodicity} = 'p' if none { $_ eq $config->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES);
 
   $::form->get_lists(printers => "ALL_PRINTERS",
                      charts   => { key       => 'ALL_CHARTS',
@@ -2033,7 +2035,8 @@ sub save_periodic_invoices_config {
 
   my $config = { active                  => $::form->{active}     ? 1 : 0,
                  terminated              => $::form->{terminated} ? 1 : 0,
-                 periodicity             => (any { $_ eq $::form->{periodicity} } qw(m q b y)) ? $::form->{periodicity} : 'm',
+                 periodicity             => (any { $_ eq $::form->{periodicity}             }       @SL::DB::PeriodicInvoicesConfig::PERIODICITIES)              ? $::form->{periodicity}             : 'm',
+                 order_value_periodicity => (any { $_ eq $::form->{order_value_periodicity} } ('p', @SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIODICITIES)) ? $::form->{order_value_periodicity} : 'p',
                  start_date_as_date      => $::form->{start_date_as_date},
                  end_date_as_date        => $::form->{end_date_as_date},
                  first_billing_date_as_date => $::form->{first_billing_date_as_date},
diff --git a/locale/de/all b/locale/de/all
index e0b5e0de0..3f8177e72 100755
--- a/locale/de/all
+++ b/locale/de/all
@@ -35,9 +35,13 @@ $self->{texts} = {
   '...on the TODO list'         => '...auf der Aufgabenliste',
   '0% tax with taxkey'          => '0% Steuer mit Steuerschl&uuml;ssel ',
   '1. Quarter'                  => '1. Quartal',
+  '2 years'                     => '2 Jahre',
   '2. Quarter'                  => '2. Quartal',
+  '3 years'                     => '3 Jahre',
   '3. Quarter'                  => '3. Quartal',
+  '4 years'                     => '4 Jahre',
   '4. Quarter'                  => '4. Quartal',
+  '5 years'                     => '5 Jahre',
   '<b> I DO CARE!</b> Please check create warehouse and bins and define a name for the warehouse (Bins will be created automatically) and then continue' => '<b>ICH KÜMMER MICH</b> Bitte haken Sie Lager und Lagerplätze erzeugen an (Automatisches Zuweisen der Lagerplätze) und vergeben einen Namen für dieses Lager (Lagerpl&auml;tze werden automatisch übernommen). Danach auf weiter.',
   '<b> I DO CARE!</b> Please click back and cancel the update and come back after there has been at least one warehouse defined with bin(s).:' => '<b>ICH KÜMMER MICH</b> Brechen Sie das Update ab und legen selber mindestens ein Lager mit Lagerplätzen unter dem Menü System / Lager an.',
   '<b> I DO NOT CARE</b> Please click continue and the following data (see list) will be deleted:' => '<b>IST MIR EGAL</b> Mit einem Klick auf Weiter (rot) werden keine Daten übernommen, bzw. migriert und die folgende Information in der untenstehenden Liste wird gelöscht.',
@@ -354,6 +358,7 @@ $self->{texts} = {
   'Billed amount'               => 'Abgerechneter Betrag',
   'Billed extra expenses'       => 'Abgerechnete Nebenkosten',
   'Billing Address'             => 'Rechnungsadresse',
+  'Billing Periodicity'         => 'Abrechnungsperiodizität',
   'Billing/shipping address (city)' => 'Rechnungsadresse (Stadt)',
   'Billing/shipping address (country)' => 'Rechnungsadresse (Land)',
   'Billing/shipping address (street)' => 'Rechnungsadresse (Straße)',
@@ -1731,6 +1736,7 @@ $self->{texts} = {
   'Order deleted!'              => 'Auftrag gelöscht!',
   'Order probability'           => 'Auftragswahrscheinlichkeit',
   'Order probability & expected billing date' => 'Auftragswahrscheinlichkeit & vorrauss. Abrechnungsdatum',
+  'Order value periodicity'     => 'Auftragswert basiert auf Periodizität',
   'Order/Item row name'         => 'Name der Auftrag-/Positions-Zeilen',
   'OrderItem'                   => 'Position',
   'Ordered'                     => 'Von Kunden bestellt',
@@ -1821,7 +1827,6 @@ $self->{texts} = {
   'Periodic inventory'          => 'Aufwandsmethode',
   'Periodic invoices active'    => 'Wiederkehrende Rechnungen aktiv',
   'Periodic invoices inactive'  => 'Wiederkehrende Rechnungen inaktiv',
-  'Periodicity'                 => 'Periodizität',
   'Perpetual inventory'         => 'Bestandsmethode',
   'Person'                      => 'Person',
   'Personal settings'           => 'Pers&ouml;nliche Einstellungen',
@@ -3240,6 +3245,7 @@ $self->{texts} = {
   'sales_order'                 => 'Kundenauftrag',
   'sales_order_list'            => 'auftragsliste',
   'sales_quotation'             => 'Verkaufsangebot',
+  'same as periodicity'         => 'stimmt mit Abrechnungsperiodizität überein',
   'saved'                       => 'gespeichert',
   'saved!'                      => 'gespeichert',
   'saving data'                 => 'Speichere Daten',
diff --git a/sql/Pg-upgrade2/periodic_invoices_order_value_periodicity.sql b/sql/Pg-upgrade2/periodic_invoices_order_value_periodicity.sql
new file mode 100644
index 000000000..d2db1d593
--- /dev/null
+++ b/sql/Pg-upgrade2/periodic_invoices_order_value_periodicity.sql
@@ -0,0 +1,26 @@
+-- @tag: periodic_invoices_order_value_periodicity
+-- @description: Wiederkehrende Rechnungen: Einstellung für Periode, auf die sich der Auftragswert bezieht
+-- @depends: release_3_1_0
+
+-- Spalte »periodicity«: nur ein Zeichen, und Check auf gültige Werte
+ALTER TABLE periodic_invoices_configs
+ADD CONSTRAINT periodic_invoices_configs_valid_periodicity
+CHECK (periodicity IN ('m', 'q', 'b', 'y'));
+
+ALTER TABLE periodic_invoices_configs
+ALTER COLUMN periodicity TYPE varchar(1);
+
+-- Neue Spalte »order_value_periodicity«
+ALTER TABLE periodic_invoices_configs
+ADD COLUMN order_value_periodicity varchar(1);
+
+UPDATE periodic_invoices_configs
+SET order_value_periodicity = 'p';
+
+ALTER TABLE periodic_invoices_configs
+ALTER COLUMN order_value_periodicity
+SET NOT NULL;
+
+ALTER TABLE periodic_invoices_configs
+ADD CONSTRAINT periodic_invoices_configs_valid_order_value_periodicity
+CHECK (order_value_periodicity IN ('p', 'm', 'q', 'b', 'y', '2', '3', '4', '5'));
diff --git a/templates/webpages/oe/edit_periodic_invoices_config.html b/templates/webpages/oe/edit_periodic_invoices_config.html
index 093cf0574..656dc4326 100644
--- a/templates/webpages/oe/edit_periodic_invoices_config.html
+++ b/templates/webpages/oe/edit_periodic_invoices_config.html
@@ -1,6 +1,7 @@
 [% USE HTML %]
 [% USE LxERP %]
 [% USE L %]
+[% SET style="width: 400px" %]
 <h1>[% title %]</h1>
 
  <form name="Form" action="oe.pl" method="post">
@@ -20,15 +21,19 @@
     </tr>
 
     <tr>
-     <th align="right" valign="top">[%- LxERP.t8('Periodicity') %]</th>
+     <th align="right" valign="top">[%- LxERP.t8('Billing Periodicity') %]</th>
      <td valign="top">
-      [% L.radio_button_tag("periodicity", value => "m", label => LxERP.t8("monthly"),   checked => periodicity == 'm') %]
-      <br>
-      [% L.radio_button_tag("periodicity", value => "q", label => LxERP.t8("every third month"), checked => periodicity == 'q') %]
-      <br>
-      [% L.radio_button_tag("periodicity", value => "b", label => LxERP.t8("semiannually"), checked => periodicity == 'b') %]
-      <br>
-      [% L.radio_button_tag("periodicity", value => "y", label => LxERP.t8("yearly"),    checked => periodicity == 'y') %]
+      [% L.select_tag("periodicity", [ [ "m", LxERP.t8("monthly") ], [ "q", LxERP.t8("every third month") ], [ "b", LxERP.t8("semiannually") ], [ "y", LxERP.t8("yearly") ] ], default=periodicity, style=style) %]
+     </td>
+    </tr>
+
+    <tr>
+     <th align="right" valign="top">[%- LxERP.t8('Order value periodicity') %]</th>
+     <td valign="top">
+      [% L.select_tag("order_value_periodicity",
+                      [ [ "p", LxERP.t8("same as periodicity") ], [ "m", LxERP.t8("monthly") ], [ "q", LxERP.t8("every third month") ], [ "b", LxERP.t8("semiannually") ], [ "y", LxERP.t8("yearly") ],
+                        [ "2", LxERP.t8("2 years") ], [ "3", LxERP.t8("3 years") ], [ "4", LxERP.t8("4 years") ], [ "5", LxERP.t8("5 years") ], ],
+                      default=order_value_periodicity, style=style) %]
      </td>
     </tr>
 
@@ -63,7 +68,7 @@
     <tr>
      <th align="right">[%- LxERP.t8('Record in') %]</th>
      <td valign="top">
-      [% L.select_tag("ar_chart_id", AR, title_key => 'description', default => ar_chart_id) %]
+      [% L.select_tag("ar_chart_id", AR, title_key => 'description', default => ar_chart_id, style=style) %]
      </td>
     </tr>
 
@@ -77,7 +82,7 @@
     <tr>
      <th align="right">[%- LxERP.t8('Printer') %]</th>
      <td valign="top">
-      [% L.select_tag("printer_id", ALL_PRINTERS, title_key = 'printer_description', default = printer_id, disabled = !print) %]
+      [% L.select_tag("printer_id", ALL_PRINTERS, title_key = 'printer_description', default = printer_id, disabled = !print, style=style) %]
      </td>
     </tr>
 
