Merge branch 'periodic-invoices-order-value-basis'
authorSven Schöling <s.schoeling@linet-services.de>
Thu, 5 Mar 2015 12:23:05 +0000 (13:23 +0100)
committerSven Schöling <s.schoeling@linet-services.de>
Thu, 5 Mar 2015 12:23:05 +0000 (13:23 +0100)
14 files changed:
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/Controller/FinancialControllingReport.pm
SL/Controller/FinancialOverview.pm
SL/DB/MetaSetup/PeriodicInvoicesConfig.pm
SL/DB/PeriodicInvoicesConfig.pm
SL/LiquidityProjection.pm
SL/OE.pm
bin/mozilla/oe.pl
locale/de/all
sql/Pg-upgrade2/periodic_invoices_order_value_periodicity.sql [new file with mode: 0644]
t/background_job/create_periodic_invoices.t [new file with mode: 0644]
t/controllers/financial_controlling/sales_order_with_periodic_invoices_config.t [new file with mode: 0644]
t/controllers/financial_overview/sales_orders.t [new file with mode: 0644]
templates/webpages/oe/edit_periodic_invoices_config.html

index d80ba4e..bdf5363 100644 (file)
@@ -66,7 +66,7 @@ sub run {
 }
 
 sub _log_msg {
-  my $message  = join('', @_);
+  my $message  = join('', 'SL::BackgroundJob::CreatePeriodicInvoices: ', @_);
   $message    .= "\n" unless $message =~ m/\n$/;
   $::lxdebug->message(LXDebug::DEBUG1(), $message);
 }
@@ -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'),
@@ -107,7 +107,7 @@ sub _generate_time_period_variables {
 sub _replace_vars {
   my (%params) = @_;
   my $sub      = $params{attribute};
-  my $str      = $params{object}->$sub;
+  my $str      = $params{object}->$sub // '';
   my $sub_fmt  = lc($params{attribute_format} // 'text');
 
   my ($start_tag, $end_tag) = $sub_fmt eq 'html' ? ('&lt;%', '%&gt;') : ('<%', '%>');
@@ -142,6 +142,40 @@ sub _replace_vars {
   $params{object}->$sub($str);
 }
 
+sub _adjust_sellprices_for_period_lengths {
+  my (%params) = @_;
+
+  my $billing_len     = $params{config}->get_billing_period_length;
+  my $order_value_len = $params{config}->get_order_value_period_length;
+
+  return if $billing_len == $order_value_len;
+
+  my $is_last_invoice_in_cycle = $params{config}->is_last_bill_date_in_order_value_cycle(date => $params{period_start_date});
+
+  _log_msg("_adjust_sellprices_for_period_lengths: period_start_date $params{period_start_date} is_last_invoice_in_cycle $is_last_invoice_in_cycle billing_len $billing_len order_value_len $order_value_len");
+
+  if ($order_value_len < $billing_len) {
+    my $num_orders_per_invoice = $billing_len / $order_value_len;
+
+    $_->sellprice($_->sellprice * $num_orders_per_invoice) for @{ $params{invoice}->items };
+
+    return;
+  }
+
+  my $num_invoices_in_cycle = $order_value_len / $billing_len;
+
+  foreach my $item (@{ $params{invoice}->items }) {
+    my $sellprice_one_invoice = $::form->round_amount($item->sellprice * $billing_len / $order_value_len, 2);
+
+    if ($is_last_invoice_in_cycle) {
+      $item->sellprice($item->sellprice - ($num_invoices_in_cycle - 1) * $sellprice_one_invoice);
+
+    } else {
+      $item->sellprice($sellprice_one_invoice);
+    }
+  }
+}
+
 sub _create_periodic_invoice {
   $main::lxdebug->enter_sub();
 
@@ -174,6 +208,8 @@ sub _create_periodic_invoice {
       _replace_vars(object => $item, vars => $time_period_vars, attribute => $_, attribute_format => ($_ eq 'longdescription' ? 'html' : 'text')) for qw(description longdescription);
     }
 
+    _adjust_sellprices_for_period_lengths(invoice => $invoice, config => $config, period_start_date => $period_start_date);
+
     $invoice->post(ar_id => $config->ar_chart_id) || die;
 
     # like $form->add_shipto, but we don't need to check for a manual exception,
@@ -210,6 +246,8 @@ sub _create_periodic_invoice {
                                  period_start_date => $period_start_date)
       ->save;
 
+    _log_msg("_create_invoice created for period start date $period_start_date id " . $invoice->id . " number " . $invoice->invnumber . " netamount " . $invoice->netamount . " amount " . $invoice->amount);
+
     # die $invoice->transaction_description;
   })) {
     $::lxdebug->message(LXDebug->WARN(), "_create_invoice failed: " . join("\n", (split(/\n/, $self->{db_obj}->db->error))[0..2]));
index c10c09d..125a8a8 100644 (file)
@@ -3,7 +3,7 @@ package SL::Controller::FinancialControllingReport;
 use strict;
 use parent qw(SL::Controller::Base);
 
-use List::Util qw(min sum);
+use List::Util qw(max min sum);
 
 use SL::DB::Order;
 use SL::DB::ProjectType;
@@ -12,8 +12,8 @@ use SL::Controller::Helper::ReportGenerator;
 use SL::Locale::String;
 
 use Rose::Object::MakeMethods::Generic (
-  scalar => [ qw(project_types) ],
-  'scalar --get_set_init' => [ qw(models) ],
+  scalar                  => [ qw(orders) ],
+  'scalar --get_set_init' => [ qw(project_types models) ],
 );
 
 __PACKAGE__->run_before(sub { $::auth->assert('sales_order_edit'); });
@@ -29,13 +29,11 @@ my %sort_columns = (
 sub action_list {
   my ($self) = @_;
 
-  $self->project_types(SL::DB::Manager::ProjectType->get_all_sorted);
-
   $self->make_filter_summary;
 
   $self->prepare_report;
 
-  $self->{orders} = $self->models->get;
+  $self->orders($self->models->get);
 
   $self->calculate_data;
 
@@ -104,7 +102,7 @@ sub prepare_report {
 sub calculate_data {
   my ($self) = @_;
 
-  foreach my $order (@{ $self->{orders} }) {
+  foreach my $order (@{ $self->orders }) {
     my @delivery_orders = @{ $order->linked_records(direction => 'to', to => 'DeliveryOrder', via => 'Order', query => [ '!customer_id' => undef ]) };
     my @invoices        = @{ $order->linked_records(direction => 'to', to => 'Invoice',       via => [ 'Order', 'DeliveryOrder' ])                  };
 
@@ -113,10 +111,10 @@ sub calculate_data {
       map({ @{ $_->storno_invoices } } grep { $_->storno && !$_->storno_id } @invoices),
     );
 
-    $order->{delivered_amount}  = sum map { $self->sum_relevant_items(order => $order, other => $_, by_order => 1)    } @delivery_orders;
-    $order->{billed_amount}     = sum map { $self->sum_relevant_items(order => $order, other => $_)                   } @invoices;
-    $order->{paid_amount}       = sum map { $_->paid * $_->netamount / (($_->amount * 1) || ($_->netamount * 1) || 1) } @invoices;
-    my $billed_amount           = sum map { $_->netamount                                                             } @invoices;
+    $order->{delivered_amount}  = sum(map { $self->sum_relevant_items(order => $order, other => $_, by_order => 1)    } @delivery_orders) // 0;
+    $order->{billed_amount}     = sum(map { $self->sum_relevant_items(order => $order, other => $_)                   } @invoices)        // 0;
+    $order->{paid_amount}       = sum(map { $_->paid * $_->netamount / (($_->amount * 1) || ($_->netamount * 1) || 1) } @invoices)        // 0;
+    my $billed_amount           = sum(map { $_->netamount                                                             } @invoices)        // 0;
     $order->{other_amount}      = $billed_amount             - $order->{billed_amount};
     $order->{billable_amount}   = $order->{delivered_amount} - $order->{billed_amount};
 
@@ -136,19 +134,24 @@ sub calculate_data {
 sub calculate_periodic_invoices_order_netamount {
   my ($self, $order) = @_;
 
+  my $year       = DateTime->today_local->year;
+  my $year_start = DateTime->new_local(day =>  1, month =>  1, year => $year);
+  my $year_end   = DateTime->new_local(day => 31, month => 12, year => $year);
+
   my $cfg        = $order->periodic_invoices_config;
-  my $num_years  = 0;
+  my $period_len = $cfg->get_billing_period_length;
+  my $num_months = 0;
   my $cur_date   = $cfg->start_date->clone;
-  my $end_date   = $cfg->terminated ? $self->end_date : undef;
-  $end_date    //= DateTime->today_local;
-  $end_date      = min($end_date, DateTime->today_local);
+  my $end_date   = $cfg->terminated ? $cfg->end_date : undef;
+  $end_date    //= $year_end;
+  $end_date      = min $end_date, $year_end;
 
   while ($cur_date <= $end_date) {
-    $num_years++;
-    $cur_date->add(years => 1);
+    $num_months += $period_len if $cur_date >= $year_start;
+    $cur_date->add(months => $period_len);
   }
 
-  return $num_years * $order->netamount * (12 / $order->periodic_invoices_config->get_period_length);
+  return $num_months * $order->netamount / $order->periodic_invoices_config->get_order_value_period_length;
 }
 
 sub sum_items {
@@ -202,7 +205,7 @@ sub list_objects {
     $data->{$_}->{data} = $::form->format_amount(\%::myconfig, $data->{$_}->{data}, 2) for grep { !m/_p$/ } @{ $self->{number_columns} };
   };
 
-  return $self->report_generator_list_objects(report => $self->{report}, objects => $self->{orders}, data_callback => $modify_data);
+  return $self->report_generator_list_objects(report => $self->{report}, objects => $self->orders, data_callback => $modify_data);
 }
 
 sub make_filter_summary {
@@ -222,6 +225,8 @@ sub make_filter_summary {
   $self->{filter_summary} = join ', ', @filter_strings;
 }
 
+sub init_project_types { SL::DB::Manager::ProjectType->get_all_sorted }
+
 sub init_models {
   my ($self) = @_;
 
index cd153d2..19ae441 100644 (file)
@@ -4,6 +4,7 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use List::MoreUtils qw(none);
+use List::Util qw(min);
 
 use SL::DB::Employee;
 use SL::DB::Invoice;
@@ -39,7 +40,7 @@ sub prepare_report {
 
   $self->report(SL::ReportGenerator->new(\%::myconfig, $::form));
 
-  my @columns = qw(year quarter month sales_quotations sales_orders sales_invoices requests_for_quotation purchase_orders purchase_invoices);
+  my @columns = (qw(year quarter month), @{ $self->types });
 
   $self->number_columns([ grep { !m/^(?:month|year|quarter)$/ } @columns ]);
 
@@ -48,14 +49,15 @@ sub prepare_report {
     year                   => { text => t8('Year')                   },
     quarter                => { text => t8('Quarter')                },
     sales_quotations       => { text => t8('Sales Quotations')       },
-    sales_orders           => { text => t8('Sales Orders')           },
+    sales_orders           => { text => t8('Sales Orders Advance')   },
+    sales_orders_per_inv   => { text => t8('Total Sales Orders Value') },
     sales_invoices         => { text => t8('Invoices')               },
     requests_for_quotation => { text => t8('Requests for Quotation') },
     purchase_orders        => { text => t8('Purchase Orders')        },
     purchase_invoices      => { text => t8('Purchase Invoices')      },
   );
 
-  map { $column_defs{$_}->{align} = 'right' } @columns;
+  $column_defs{$_}->{align} = 'right' for @columns;
 
   $self->report->set_options(
     std_column_visibility => 1,
@@ -87,6 +89,7 @@ sub get_objects {
   $self->objects({
     sales_quotations       => SL::DB::Manager::Order->get_all(          where => [ and => [ @f_date, @f_salesman, SL::DB::Manager::Order->type_filter('sales_quotation')   ]]),
     sales_orders           => SL::DB::Manager::Order->get_all(          where => [ and => [ @f_date, @f_salesman, SL::DB::Manager::Order->type_filter('sales_order')       ]], with_objects => [ qw(periodic_invoices_config) ]),
+    sales_orders_per_inv   => [],
     requests_for_quotation => SL::DB::Manager::Order->get_all(          where => [ and => [ @f_date, @f_salesman, SL::DB::Manager::Order->type_filter('request_quotation') ]]),
     purchase_orders        => SL::DB::Manager::Order->get_all(          where => [ and => [ @f_date, @f_salesman, SL::DB::Manager::Order->type_filter('purchase_order')    ]]),
     sales_invoices         => SL::DB::Manager::Invoice->get_all(        where => [ and => [ @f_date, @f_salesman, ]]),
@@ -97,7 +100,7 @@ sub get_objects {
   $self->objects->{sales_orders} = [ grep { !$_->periodic_invoices_config || !$_->periodic_invoices_config->active } @{ $self->objects->{sales_orders} } ];
 }
 
-sub init_types { [ qw(sales_quotations sales_orders sales_invoices requests_for_quotation purchase_orders purchase_invoices) ] }
+sub init_types { [ qw(sales_quotations sales_orders sales_orders_per_inv sales_invoices requests_for_quotation purchase_orders purchase_invoices) ] }
 
 sub init_data {
   my ($self) = @_;
@@ -122,7 +125,8 @@ sub calculate_one_time_data {
   my ($self) = @_;
 
   foreach my $type (@{ $self->types }) {
-    foreach my $object (@{ $self->objects->{ $type } }) {
+    my $src_object_type = $type eq 'sales_orders_per_inv' ? 'sales_orders' : $type;
+    foreach my $object (@{ $self->objects->{ $src_object_type } }) {
       my $month                              = $object->transdate->month - 1;
       my $tdata                              = $self->data->{$type};
 
@@ -145,15 +149,25 @@ sub calculate_periodic_invoices {
 sub calculate_one_periodic_invoice {
   my ($self, %params) = @_;
 
-  return if $params{config}->start_date > $params{end_date};
+  # Calculate sales order advance
+  my $net  = $params{config}->order->netamount * $params{config}->get_billing_period_length / $params{config}->get_order_value_period_length;
+  my $sord = $self->data->{sales_orders};
 
-  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 $sord       = $self->data->{sales_orders};
+  foreach my $date ($params{config}->calculate_invoice_dates(start_date => $params{start_date}, end_date => $params{end_date}, past_dates => 1)) {
+    $sord->{months  }->[ $date->month   - 1 ] += $net;
+    $sord->{quarters}->[ $date->quarter - 1 ] += $net;
+    $sord->{year}                             += $net;
+  }
+
+  # Calculate total sales order value
+  my $date = $params{config}->order->transdate;
+  return if $date->year != $params{start_date}->year;
 
-  $sord->{months  }->[ $first_date->month   - 1 ] += $net;
-  $sord->{quarters}->[ $first_date->quarter - 1 ] += $net;
-  $sord->{year}                                   += $net;
+  $net                                       = $params{config}->order->netamount;
+  $sord                                      = $self->data->{sales_orders_per_inv};
+  $sord->{months  }->[ $date->month   - 1 ] += $net;
+  $sord->{quarters}->[ $date->quarter - 1 ] += $net;
+  $sord->{year}                             += $net;
 }
 
 sub list_data {
index eae8481..1aeb42f 100644 (file)
@@ -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' },
index 8013cd9..0d66085 100644 (file)
@@ -11,16 +11,24 @@ __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('', @_));
+  $::lxdebug->message(LXDebug->DEBUG1(), join('', 'SL::DB::PeriodicInvoicesConfig: ', @_));
 }
 
 sub handle_automatic_extension {
@@ -78,12 +86,12 @@ SQL
 sub calculate_invoice_dates {
   my ($self, %params) = @_;
 
-  my $period_len = $self->get_period_length;
-  my $cur_date   = $self->first_billing_date || $self->start_date;
+  my $period_len = $self->get_billing_period_length;
+  my $cur_date   = ($self->first_billing_date || $self->start_date)->clone;
   my $end_date   = $self->terminated ? $self->end_date : undef;
   $end_date    //= DateTime->today_local->add(years => 100);
-  my $start_date = $params{past_dates} ? undef                       : $self->get_previous_billed_period_start_date;
-  $start_date    = $start_date         ? $start_date->add(days => 1) : $cur_date->clone;
+  my $start_date = $params{past_dates} ? undef                              : $self->get_previous_billed_period_start_date;
+  $start_date    = $start_date         ? $start_date->clone->add(days => 1) : $cur_date->clone;
 
   $start_date    = max($start_date, $params{start_date}) if $params{start_date};
   $end_date      = min($end_date,   $params{end_date})   if $params{end_date};
@@ -99,4 +107,142 @@ sub calculate_invoice_dates {
   return @dates;
 }
 
+sub is_last_bill_date_in_order_value_cycle {
+  my ($self, %params)    = @_;
+
+  my $months_billing     = $self->get_billing_period_length;
+  my $months_order_value = $self->get_order_value_period_length;
+
+  return 1 if $months_billing >= $months_order_value;
+
+  my $next_billing_date = $params{date}->clone->add(months => $months_billing);
+  my $date_itr          = max($self->start_date, $self->first_billing_date || $self->start_date)->clone;
+
+  _log_msg("is_last_billing_date_in_order_value_cycle start: id " . $self->id . " date_itr $date_itr start " . $self->start_date);
+
+  $date_itr->add(months => $months_order_value) while $date_itr < $next_billing_date;
+
+  _log_msg("is_last_billing_date_in_order_value_cycle end: refdate $params{date} next_billing_date $next_billing_date date_itr $date_itr months_billing $months_billing months_order_value $months_order_value result "
+           . ($date_itr == $next_billing_date));
+
+  return $date_itr == $next_billing_date;
+}
+
 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.
+
+=item C<is_last_billing_date_in_order_value_cycle %params>
+
+Determines whether or not the mandatory parameter C<date>, an instance
+of L<DateTime>, is the last billing date within the cycle given by the
+order value periodicity. Returns a truish value if this is the case
+and a falsish value otherwise.
+
+This check is always true if the billing periodicity is longer than or
+equal to the order value periodicity. For example, if you have an
+order whose value is given for three months and you bill every six
+months and you have twice the order value on each invoice, meaning
+each invoice is itself the last invoice for not only one but two order
+value cycles.
+
+Otherwise (if the order value periodicity is longer than the billing
+periodicity) this function iterates over all eligible dates starting
+with C<first_billing_date> (or C<start_date> if C<first_billing_date>
+is unset) and adding the order value length with each step. If the
+date given by the C<date> parameter plus the billing period length
+equals one of those dates then the given date is indeed the date of
+the last invoice in that particular order value cycle.
+
+=back
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 03a1894..710c099 100644 (file)
@@ -5,6 +5,7 @@ use strict;
 use List::MoreUtils qw(uniq);
 
 use SL::DBUtils;
+use SL::DB::PeriodicInvoicesConfig;
 
 sub new {
   my $package       = shift;
@@ -128,7 +129,7 @@ SQL
     SELECT (oi.qty * (1 - oi.discount) * oi.sellprice) AS linetotal,
       bg.description AS buchungsgruppe,
       CASE WHEN COALESCE(e.name, '') = '' THEN e.login ELSE e.name END AS salesman,
-      pcfg.periodicity, pcfg.id AS config_id,
+      pcfg.periodicity, pcfg.order_value_periodicity, pcfg.id AS config_id,
       EXTRACT(year FROM pcfg.start_date) AS start_year, EXTRACT(month FROM pcfg.start_date) AS start_month
     FROM orderitems oi
     LEFT JOIN oe                             ON (oi.trans_id                              = oe.id)
@@ -140,7 +141,6 @@ SQL
 SQL
 
   # 3. Iterieren über Saldierungsintervalle, vormerken
-  my %periodicities = ( 'm' => 1, 'q' => 3,  'y' => 12 );
   my @scentries;
   $sth = prepare_execute_query($::form, $dbh, $query);
   while ($ref = $sth->fetchrow_hashref) {
@@ -148,15 +148,20 @@ SQL
     my $date;
 
     while (($date = _the_date($year, $month)) le $self->{max_date}) {
+      my $billing_len = $SL::DB::PeriodicInvoicesConfig::PERIOD_LENGTHS{ $ref->{periodicity} } || 1;
+
       if (($date ge $self->{min_date}) && (!$periodic_invoices{ $ref->{config_id} } || !$periodic_invoices{ $ref->{config_id} }->{$date})) {
+        my $order_value_periodicity = $ref->{order_value_periodicity} eq 'p' ? $ref->{periodicity} : $ref->{order_value_periodicity};
+        my $order_value_len         = $SL::DB::PeriodicInvoicesConfig::ORDER_VALUE_PERIOD_LENGTHS{$order_value_periodicity} || 1;
+
         push @scentries, { buchungsgruppe => $ref->{buchungsgruppe},
                            salesman       => $ref->{salesman},
-                           linetotal      => $ref->{linetotal},
+                           linetotal      => $ref->{linetotal} * $billing_len / $order_value_len,
                            date           => $date,
                          };
       }
 
-      ($year, $month) = _fix_date($year, $month + ($periodicities{ $ref->{periodicity} } || 1));
+      ($year, $month) = _fix_date($year, $month + $billing_len);
     }
   }
   $sth->finish;
index 061cd27..9944f71 100644 (file)
--- 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);
     }
index 4483deb..147e9d9 100644 (file)
@@ -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},
index e0b5e0d..ffd4375 100755 (executable)
@@ -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',
@@ -2135,6 +2140,7 @@ $self->{texts} = {
   'Sales Invoices'              => 'Kundenrechnungen',
   'Sales Order'                 => 'Kundenauftrag',
   'Sales Orders'                => 'Aufträge',
+  'Sales Orders Advance'        => 'Auftragsvorlauf',
   'Sales Orders deleteable'     => 'Kundenaufträge löschbar',
   'Sales Price Rules'           => 'Preisregeln Verkauf',
   'Sales Price Rules '          => 'Preisregeln (Verkauf)',
@@ -2813,6 +2819,7 @@ $self->{texts} = {
   'Top Level Designation only'  => 'Nur Hauptartikelbezeichnung',
   'Total'                       => 'Summe',
   'Total Fees'                  => 'Kumulierte Gebühren',
+  'Total Sales Orders Value'    => 'Auftragseingang',
   'Total stock value'           => 'Gesamter Bestandswert',
   'Total sum'                   => 'Gesamtsumme',
   'Total weight'                => 'Gesamtgewicht',
@@ -3240,6 +3247,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 (file)
index 0000000..d2db1d5
--- /dev/null
@@ -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/t/background_job/create_periodic_invoices.t b/t/background_job/create_periodic_invoices.t
new file mode 100644 (file)
index 0000000..8d1c170
--- /dev/null
@@ -0,0 +1,240 @@
+package DateTime;
+
+use SL::Helper::DateTime;
+
+no warnings 'redefine';
+
+sub now_local {
+  return shift->new(time_zone => $::locale->get_local_time_zone, year => 2014, month => 3, day => 15, hour => 12, minute => 23, second => 34);
+}
+
+sub today_local {
+  return shift->now_local->truncate(to => 'day');
+}
+
+package main;
+
+use Test::More tests => 80;
+
+use lib 't';
+use strict;
+use utf8;
+
+use Carp;
+use Support::TestSetup;
+
+use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
+use_ok 'SL::DB::Chart';
+use_ok 'SL::DB::Customer';
+use_ok 'SL::DB::Default';
+use_ok 'SL::DB::Invoice';
+use_ok 'SL::DB::Order';
+use_ok 'SL::DB::Part';
+use_ok 'SL::DB::TaxZone';
+
+Support::TestSetup::login();
+
+our ($ar_chart, $buchungsgruppe, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+
+sub init_common_state {
+  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
+  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
+  $currency_id    = SL::DB::Default->get->currency_id;
+  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
+  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
+  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+}
+
+sub create_invoices {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(customer part tax order orderitem periodic_invoices_config);
+
+  # Clean up: remove invoices, orders, parts and customers
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+
+  $customer     = SL::DB::Customer->new(
+    name        => 'Test Customer',
+    currency_id => $currency_id,
+    taxzone_id  => $tax_zone->id,
+    %{ $params{customer} }
+  )->save;
+
+  $part = SL::DB::Part->new(
+    partnumber         => 'T4254',
+    description        => 'Fourty-two fifty-four',
+    lastcost           => 222.22,
+    sellprice          => 333.33,
+    buchungsgruppen_id => $buchungsgruppe->id,
+    unit               => $unit->name,
+    %{ $params{part} }
+  )->save;
+  $part->load;
+
+  $order                     = SL::DB::Order->new(
+    customer_id              => $customer->id,
+    currency_id              => $currency_id,
+    taxzone_id               => $tax_zone->id,
+    transaction_description  => '<%period_start_date%>',
+    orderitems               => [
+      { parts_id             => $part->id,
+        description          => $part->description,
+        lastcost             => $part->lastcost,
+        sellprice            => $part->sellprice,
+        qty                  => 1,
+        unit                 => $unit->name,
+        %{ $params{orderitem} },
+      },
+    ],
+    periodic_invoices_config => {
+      active                 => 1,
+      ar_chart_id            => $ar_chart->id,
+      %{ $params{periodic_invoices_config} },
+    },
+    %{ $params{order} },
+  );
+
+  $order->calculate_prices_and_taxes;
+
+  ok($order->save(cascade => 1));
+
+  SL::BackgroundJob::CreatePeriodicInvoices->new->run(SL::DB::BackgroundJob->new);
+
+  @invoices = @{ SL::DB::Manager::Invoice->get_all(sort_by => [ qw(id) ]) };
+}
+
+sub are_invoices {
+  my ($description, @exp_date_netamount_pairs) = @_;
+
+  is scalar(@invoices), scalar(@exp_date_netamount_pairs), "${description} number of invoices " . scalar(@exp_date_netamount_pairs);
+
+  my @actual_date_netamount_pairs = map { [ $_->transaction_description, $_->netamount * 1 ] } @invoices;
+  is_deeply \@actual_date_netamount_pairs, \@exp_date_netamount_pairs, "${description} date/netamount of created invoices";
+}
+
+init_common_state();
+
+# order_value_periodicity=y
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => 'y', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=m ovp=y',[ '01.01.2013', 27.78 ], [ '01.02.2013', 27.78 ], [ '01.03.2013', 27.78 ], [ '01.04.2013', 27.78 ],
+                         [ '01.05.2013', 27.78 ], [ '01.06.2013', 27.78 ], [ '01.07.2013', 27.78 ], [ '01.08.2013', 27.78 ],
+                         [ '01.09.2013', 27.78 ], [ '01.10.2013', 27.78 ], [ '01.11.2013', 27.78 ], [ '01.12.2013', 27.75 ],
+                         [ '01.01.2014', 27.78 ], [ '01.02.2014', 27.78 ], [ '01.03.2014', 27.78 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => 'y', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=q ovp=y',[ '01.01.2013', 83.33 ], [ '01.04.2013', 83.33 ], [ '01.07.2013', 83.33 ], [ '01.10.2013', 83.34 ], [ '01.01.2014', 83.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => 'y', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=b ovp=y',[ '01.01.2013', 166.67 ], [ '01.07.2013', 166.66 ], [ '01.01.2014', 166.67 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => 'y', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=y ovp=y',[ '01.01.2013', 333.33 ], [ '01.01.2014', 333.33 ];
+
+# order_value_periodicity=b
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => 'b', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=m ovp=b',[ '01.01.2013', 55.56 ], [ '01.02.2013', 55.56 ], [ '01.03.2013', 55.56 ], [ '01.04.2013', 55.56 ],
+                         [ '01.05.2013', 55.56 ], [ '01.06.2013', 55.53 ], [ '01.07.2013', 55.56 ], [ '01.08.2013', 55.56 ],
+                         [ '01.09.2013', 55.56 ], [ '01.10.2013', 55.56 ], [ '01.11.2013', 55.56 ], [ '01.12.2013', 55.53 ],
+                         [ '01.01.2014', 55.56 ], [ '01.02.2014', 55.56 ], [ '01.03.2014', 55.56 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => 'b', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=q ovp=b',[ '01.01.2013', 166.67 ], [ '01.04.2013', 166.66 ], [ '01.07.2013', 166.67 ], [ '01.10.2013', 166.66 ], [ '01.01.2014', 166.67 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => 'b', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=b ovp=b',[ '01.01.2013', 333.33 ], [ '01.07.2013', 333.33 ], [ '01.01.2014', 333.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => 'b', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=y ovp=b',[ '01.01.2013', 666.66 ], [ '01.01.2014', 666.66 ];
+
+# order_value_periodicity=q
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => 'q', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=m ovp=q',[ '01.01.2013', 111.11 ], [ '01.02.2013', 111.11 ], [ '01.03.2013', 111.11 ], [ '01.04.2013', 111.11 ],
+                         [ '01.05.2013', 111.11 ], [ '01.06.2013', 111.11 ], [ '01.07.2013', 111.11 ], [ '01.08.2013', 111.11 ],
+                         [ '01.09.2013', 111.11 ], [ '01.10.2013', 111.11 ], [ '01.11.2013', 111.11 ], [ '01.12.2013', 111.11 ],
+                         [ '01.01.2014', 111.11 ], [ '01.02.2014', 111.11 ], [ '01.03.2014', 111.11 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => 'q', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=q ovp=q',[ '01.01.2013', 333.33 ], [ '01.04.2013', 333.33 ], [ '01.07.2013', 333.33 ], [ '01.10.2013', 333.33 ], [ '01.01.2014', 333.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => 'q', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=b ovp=q',[ '01.01.2013', 666.66 ], [ '01.07.2013', 666.66 ], [ '01.01.2014', 666.66 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => 'q', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=y ovp=q',[ '01.01.2013', 1333.32 ], [ '01.01.2014', 1333.32 ];
+
+# order_value_periodicity=m
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => 'm', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=m ovp=m',[ '01.01.2013', 333.33 ], [ '01.02.2013', 333.33 ], [ '01.03.2013', 333.33 ], [ '01.04.2013', 333.33 ],
+                         [ '01.05.2013', 333.33 ], [ '01.06.2013', 333.33 ], [ '01.07.2013', 333.33 ], [ '01.08.2013', 333.33 ],
+                         [ '01.09.2013', 333.33 ], [ '01.10.2013', 333.33 ], [ '01.11.2013', 333.33 ], [ '01.12.2013', 333.33 ],
+                         [ '01.01.2014', 333.33 ], [ '01.02.2014', 333.33 ], [ '01.03.2014', 333.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => 'm', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=q ovp=m',[ '01.01.2013', 999.99 ], [ '01.04.2013', 999.99 ], [ '01.07.2013', 999.99 ], [ '01.10.2013', 999.99 ], [ '01.01.2014', 999.99 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => 'm', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=b ovp=m',[ '01.01.2013', 1999.98 ], [ '01.07.2013', 1999.98 ], [ '01.01.2014', 1999.98 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => 'm', start_date => DateTime->from_kivitendo('01.01.2013') });
+are_invoices 'p=y ovp=m',[ '01.01.2013', 3999.96 ], [ '01.01.2014', 3999.96 ];
+
+# order_value_periodicity=2
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => '2', start_date => DateTime->from_kivitendo('01.01.2012') });
+are_invoices 'p=m ovp=2',[ '01.01.2012', 13.89 ], [ '01.02.2012', 13.89 ], [ '01.03.2012', 13.89 ], [ '01.04.2012', 13.89 ],
+                         [ '01.05.2012', 13.89 ], [ '01.06.2012', 13.89 ], [ '01.07.2012', 13.89 ], [ '01.08.2012', 13.89 ],
+                         [ '01.09.2012', 13.89 ], [ '01.10.2012', 13.89 ], [ '01.11.2012', 13.89 ], [ '01.12.2012', 13.89 ],
+                         [ '01.01.2013', 13.89 ], [ '01.02.2013', 13.89 ], [ '01.03.2013', 13.89 ], [ '01.04.2013', 13.89 ],
+                         [ '01.05.2013', 13.89 ], [ '01.06.2013', 13.89 ], [ '01.07.2013', 13.89 ], [ '01.08.2013', 13.89 ],
+                         [ '01.09.2013', 13.89 ], [ '01.10.2013', 13.89 ], [ '01.11.2013', 13.89 ], [ '01.12.2013', 13.86 ],
+                         [ '01.01.2014', 13.89 ], [ '01.02.2014', 13.89 ], [ '01.03.2014', 13.89 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => '2', start_date => DateTime->from_kivitendo('01.01.2012') });
+are_invoices 'p=q ovp=2',[ '01.01.2012', 41.67 ], [ '01.04.2012', 41.67 ], [ '01.07.2012', 41.67 ], [ '01.10.2012', 41.67 ],
+                         [ '01.01.2013', 41.67 ], [ '01.04.2013', 41.67 ], [ '01.07.2013', 41.67 ], [ '01.10.2013', 41.64 ],
+                         [ '01.01.2014', 41.67 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => '2', start_date => DateTime->from_kivitendo('01.01.2012') });
+are_invoices 'p=b ovp=2',[ '01.01.2012', 83.33 ], [ '01.07.2012', 83.33 ], [ '01.01.2013', 83.33 ], [ '01.07.2013', 83.34 ], [ '01.01.2014', 83.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => '2', start_date => DateTime->from_kivitendo('01.01.2012') });
+are_invoices 'p=y ovp=2',[ '01.01.2012', 166.67 ], [ '01.01.2013', 166.66 ], [ '01.01.2014', 166.67 ];
+
+# order_value_periodicity=5
+create_invoices(periodic_invoices_config => { periodicity => 'm', order_value_periodicity => '5', start_date => DateTime->from_kivitendo('01.01.2009') });
+are_invoices 'p=m ovp=5',[ '01.01.2009',  5.56 ], [ '01.02.2009',  5.56 ], [ '01.03.2009',  5.56 ], [ '01.04.2009',  5.56 ],
+                         [ '01.05.2009',  5.56 ], [ '01.06.2009',  5.56 ], [ '01.07.2009',  5.56 ], [ '01.08.2009',  5.56 ],
+                         [ '01.09.2009',  5.56 ], [ '01.10.2009',  5.56 ], [ '01.11.2009',  5.56 ], [ '01.12.2009',  5.56 ],
+                         [ '01.01.2010',  5.56 ], [ '01.02.2010',  5.56 ], [ '01.03.2010',  5.56 ], [ '01.04.2010',  5.56 ],
+                         [ '01.05.2010',  5.56 ], [ '01.06.2010',  5.56 ], [ '01.07.2010',  5.56 ], [ '01.08.2010',  5.56 ],
+                         [ '01.09.2010',  5.56 ], [ '01.10.2010',  5.56 ], [ '01.11.2010',  5.56 ], [ '01.12.2010',  5.56 ],
+                         [ '01.01.2011',  5.56 ], [ '01.02.2011',  5.56 ], [ '01.03.2011',  5.56 ], [ '01.04.2011',  5.56 ],
+                         [ '01.05.2011',  5.56 ], [ '01.06.2011',  5.56 ], [ '01.07.2011',  5.56 ], [ '01.08.2011',  5.56 ],
+                         [ '01.09.2011',  5.56 ], [ '01.10.2011',  5.56 ], [ '01.11.2011',  5.56 ], [ '01.12.2011',  5.56 ],
+                         [ '01.01.2012',  5.56 ], [ '01.02.2012',  5.56 ], [ '01.03.2012',  5.56 ], [ '01.04.2012',  5.56 ],
+                         [ '01.05.2012',  5.56 ], [ '01.06.2012',  5.56 ], [ '01.07.2012',  5.56 ], [ '01.08.2012',  5.56 ],
+                         [ '01.09.2012',  5.56 ], [ '01.10.2012',  5.56 ], [ '01.11.2012',  5.56 ], [ '01.12.2012',  5.56 ],
+                         [ '01.01.2013',  5.56 ], [ '01.02.2013',  5.56 ], [ '01.03.2013',  5.56 ], [ '01.04.2013',  5.56 ],
+                         [ '01.05.2013',  5.56 ], [ '01.06.2013',  5.56 ], [ '01.07.2013',  5.56 ], [ '01.08.2013',  5.56 ],
+                         [ '01.09.2013',  5.56 ], [ '01.10.2013',  5.56 ], [ '01.11.2013',  5.56 ], [ '01.12.2013',  5.29 ],
+                         [ '01.01.2014',  5.56 ], [ '01.02.2014',  5.56 ], [ '01.03.2014',  5.56 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'q', order_value_periodicity => '5', start_date => DateTime->from_kivitendo('01.01.2009') });
+are_invoices 'p=q ovp=5',[ '01.01.2009', 16.67 ], [ '01.04.2009', 16.67 ], [ '01.07.2009', 16.67 ], [ '01.10.2009', 16.67 ],
+                         [ '01.01.2010', 16.67 ], [ '01.04.2010', 16.67 ], [ '01.07.2010', 16.67 ], [ '01.10.2010', 16.67 ],
+                         [ '01.01.2011', 16.67 ], [ '01.04.2011', 16.67 ], [ '01.07.2011', 16.67 ], [ '01.10.2011', 16.67 ],
+                         [ '01.01.2012', 16.67 ], [ '01.04.2012', 16.67 ], [ '01.07.2012', 16.67 ], [ '01.10.2012', 16.67 ],
+                         [ '01.01.2013', 16.67 ], [ '01.04.2013', 16.67 ], [ '01.07.2013', 16.67 ], [ '01.10.2013', 16.60 ],
+                         [ '01.01.2014', 16.67 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'b', order_value_periodicity => '5', start_date => DateTime->from_kivitendo('01.01.2009') });
+are_invoices 'p=b ovp=5',[ '01.01.2009', 33.33 ], [ '01.07.2009', 33.33 ],
+                         [ '01.01.2010', 33.33 ], [ '01.07.2010', 33.33 ],
+                         [ '01.01.2011', 33.33 ], [ '01.07.2011', 33.33 ],
+                         [ '01.01.2012', 33.33 ], [ '01.07.2012', 33.33 ],
+                         [ '01.01.2013', 33.33 ], [ '01.07.2013', 33.36 ],
+                         [ '01.01.2014', 33.33 ];
+
+create_invoices(periodic_invoices_config => { periodicity => 'y', order_value_periodicity => '5', start_date => DateTime->from_kivitendo('01.01.2009') });
+are_invoices 'p=y ovp=5',[ '01.01.2009', 66.67 ], [ '01.01.2010', 66.67 ], [ '01.01.2011', 66.67 ], [ '01.01.2012', 66.67 ], [ '01.01.2013', 66.65 ], [ '01.01.2014', 66.67 ];
+
+done_testing();
diff --git a/t/controllers/financial_controlling/sales_order_with_periodic_invoices_config.t b/t/controllers/financial_controlling/sales_order_with_periodic_invoices_config.t
new file mode 100644 (file)
index 0000000..1dab1e8
--- /dev/null
@@ -0,0 +1,548 @@
+package DateTime;
+
+use SL::Helper::DateTime;
+
+no warnings 'redefine';
+
+sub now_local {
+  return shift->new(time_zone => $::locale->get_local_time_zone, year => 2014, month => 3, day => 15, hour => 12, minute => 23, second => 34);
+}
+
+sub today_local {
+  return shift->now_local->truncate(to => 'day');
+}
+
+package main;
+
+use Test::More; # tests => 49;
+
+use lib 't';
+use strict;
+use utf8;
+
+use Carp;
+use Support::TestSetup;
+
+use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
+use_ok 'SL::Controller::FinancialControllingReport';
+use_ok 'SL::DB::Chart';
+use_ok 'SL::DB::Customer';
+use_ok 'SL::DB::Default';
+use_ok 'SL::DB::Invoice';
+use_ok 'SL::DB::Order';
+use_ok 'SL::DB::Part';
+use_ok 'SL::DB::TaxZone';
+
+Support::TestSetup::login();
+
+our ($ar_chart, $buchungsgruppe, $ctrl, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+
+sub init_common_state {
+  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
+  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
+  $currency_id    = SL::DB::Default->get->currency_id;
+  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
+  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
+  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+}
+
+sub create_sales_order {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(customer part tax order orderitem);
+
+  # Clean up: remove invoices, orders, parts and customers
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+
+  $customer     = SL::DB::Customer->new(
+    name        => 'Test Customer',
+    currency_id => $currency_id,
+    taxzone_id  => $tax_zone->id,
+    %{ $params{customer} }
+  )->save;
+
+  $part = SL::DB::Part->new(
+    partnumber         => 'T4254',
+    description        => 'Fourty-two fifty-four',
+    lastcost           => 222.22,
+    sellprice          => 333.33,
+    buchungsgruppen_id => $buchungsgruppe->id,
+    unit               => $unit->name,
+    %{ $params{part} }
+  )->save;
+  $part->load;
+
+  $order                     = SL::DB::Order->new(
+    customer_id              => $customer->id,
+    currency_id              => $currency_id,
+    taxzone_id               => $tax_zone->id,
+    transaction_description  => '<%period_start_date%>',
+    transdate                => DateTime->from_kivitendo('01.03.2014'),
+    orderitems               => [
+      { parts_id             => $part->id,
+        description          => $part->description,
+        lastcost             => $part->lastcost,
+        sellprice            => $part->sellprice,
+        qty                  => 1,
+        unit                 => $unit->name,
+        %{ $params{orderitem} },
+      },
+    ],
+    periodic_invoices_config => $params{periodic_invoices_config} ? {
+      active                 => 1,
+      ar_chart_id            => $ar_chart->id,
+      %{ $params{periodic_invoices_config} },
+    } : undef,
+    %{ $params{order} },
+  );
+
+  $order->calculate_prices_and_taxes;
+
+  ok($order->save(cascade => 1));
+
+  $::form = Form->new('');
+  $ctrl   = SL::Controller::FinancialControllingReport->new;
+
+  $ctrl->orders($ctrl->models->get);
+  $ctrl->calculate_data;
+}
+
+my @columns = qw(net_amount         other_amount
+                 delivered_amount   billed_amount   paid_amount   billable_amount
+                 delivered_amount_p billed_amount_p paid_amount_p billable_amount_p);
+
+sub run_tests {
+  my ($msg, $num_orders, $values, %order_params) = @_;
+
+  create_sales_order(%order_params);
+
+  is($num_orders, scalar @{ $ctrl->orders }, "${msg}, #orders");
+  is_deeply([ map { ($ctrl->orders->[0]->{$_} // 0) * 1 } @columns ],
+            $values,
+            "${msg}, values");
+}
+
+init_common_state();
+
+# ----------------------------------------------------------------------
+# An order without periodic invoices:
+run_tests("no periodic conf", 1, [ 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]);
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q
+
+run_tests(
+  "periodic conf p=q ovp=y, starting in previous year", 1,
+  [ 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting and ending in previous year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+    end_date                => DateTime->from_kivitendo('01.12.2013'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting in next year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2015'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting January 1st of current year", 1,
+  [ 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting July 1st of current year", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting May 1st of current year", 1,
+  [ 249.9975, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting January 1st of current year, ending June 30", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting July 1st of current year, ending November 30", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+    end_date                => DateTime->from_kivitendo('30.11.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting May 1st of current year, ending next year", 1,
+  [ 249.9975, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2015'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=y, starting November 1 in previous year, ending April 30", 1,
+  [ 83.3325, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.11.2013'),
+    end_date                => DateTime->from_kivitendo('30.04.2014'),
+  });
+
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=m
+
+run_tests(
+  "periodic conf p=m ovp=y, starting in previous year", 1,
+  [ 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting and ending in previous year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+    end_date                => DateTime->from_kivitendo('01.12.2013'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting in next year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2015'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting January 1st of current year", 1,
+  [ 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting July 1st of current year", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting May 1st of current year", 1,
+  [ 222.22, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting January 1st of current year, ending June 30", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting July 1st of current year, ending November 30", 1,
+  [ 138.8875, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+    end_date                => DateTime->from_kivitendo('30.11.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting May 1st of current year, ending next year", 1,
+  [ 222.22, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2015'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=m ovp=y, starting November 1 in previous year, ending April 30", 1,
+  [ 111.11, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.11.2013'),
+    end_date                => DateTime->from_kivitendo('30.04.2014'),
+  });
+
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q
+
+run_tests(
+  "periodic conf p=q ovp=2, starting in previous year", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting and ending in previous year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+    end_date                => DateTime->from_kivitendo('01.12.2013'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting in next year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.01.2015'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting January 1st of current year", 1,
+  [ 166.665, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting July 1st of current year", 1,
+  [ 83.3325, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting May 1st of current year", 1,
+  [ 124.99875, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting January 1st of current year, ending June 30", 1,
+  [ 83.3325, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting July 1st of current year, ending November 30", 1,
+  [ 83.3325, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+    end_date                => DateTime->from_kivitendo('30.11.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting May 1st of current year, ending next year", 1,
+  [ 124.99875, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2015'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=q ovp=2, starting November 1 in previous year, ending April 30", 1,
+  [ 41.66625, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => '2',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.11.2013'),
+    end_date                => DateTime->from_kivitendo('30.04.2014'),
+  });
+
+
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=m, periodicity=b
+
+run_tests(
+  "periodic conf p=b ovp=m, starting in previous year", 1,
+  [ 3999.96, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting and ending in previous year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+    end_date                => DateTime->from_kivitendo('01.12.2013'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting in next year", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.01.2015'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting January 1st of current year", 1,
+  [ 3999.96, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting July 1st of current year", 1,
+  [ 1999.98, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting May 1st of current year", 1,
+  [ 3999.96, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting January 1st of current year, ending June 30", 1,
+  [ 1999.98, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.01.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting July 1st of current year, ending November 30", 1,
+  [ 1999.98, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.07.2014'),
+    end_date                => DateTime->from_kivitendo('30.11.2014'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting May 1st of current year, ending next year", 1,
+  [ 3999.96, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+    end_date                => DateTime->from_kivitendo('30.06.2015'),
+    terminated              => 1,
+  });
+
+run_tests(
+  "periodic conf p=b ovp=m, starting November 1 in previous year, ending April 30", 1,
+  [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
+  periodic_invoices_config  => {
+    periodicity             => 'b',
+    order_value_periodicity => 'm',
+    terminated              => 1,
+    start_date              => DateTime->from_kivitendo('01.11.2013'),
+    end_date                => DateTime->from_kivitendo('30.04.2014'),
+  });
+
+
+done_testing();
diff --git a/t/controllers/financial_overview/sales_orders.t b/t/controllers/financial_overview/sales_orders.t
new file mode 100644 (file)
index 0000000..9b278be
--- /dev/null
@@ -0,0 +1,206 @@
+package DateTime;
+
+use SL::Helper::DateTime;
+
+no warnings 'redefine';
+
+sub now_local {
+  return shift->new(time_zone => $::locale->get_local_time_zone, year => 2014, month => 3, day => 15, hour => 12, minute => 23, second => 34);
+}
+
+sub today_local {
+  return shift->now_local->truncate(to => 'day');
+}
+
+package main;
+
+use Test::More tests => 49;
+
+use lib 't';
+use strict;
+use utf8;
+
+use Carp;
+use Support::TestSetup;
+
+use_ok 'SL::BackgroundJob::CreatePeriodicInvoices';
+use_ok 'SL::Controller::FinancialOverview';
+use_ok 'SL::DB::Chart';
+use_ok 'SL::DB::Customer';
+use_ok 'SL::DB::Default';
+use_ok 'SL::DB::Invoice';
+use_ok 'SL::DB::Order';
+use_ok 'SL::DB::Part';
+use_ok 'SL::DB::TaxZone';
+
+Support::TestSetup::login();
+
+our ($ar_chart, $buchungsgruppe, $ctrl, $currency_id, $customer, $employee, $order, $part, $tax_zone, $unit, @invoices);
+
+sub init_common_state {
+  $ar_chart       = SL::DB::Manager::Chart->find_by(accno => '1400')                        || croak "No AR chart";
+  $buchungsgruppe = SL::DB::Manager::Buchungsgruppe->find_by(description => 'Standard 19%') || croak "No accounting group";
+  $currency_id    = SL::DB::Default->get->currency_id;
+  $employee       = SL::DB::Manager::Employee->current                                      || croak "No employee";
+  $tax_zone       = SL::DB::Manager::TaxZone->find_by( description => 'Inland')             || croak "No taxzone";
+  $unit           = SL::DB::Manager::Unit->find_by(name => 'psch')                          || croak "No unit";
+}
+
+sub create_sales_order {
+  my %params = @_;
+
+  $params{$_} ||= {} for qw(customer part tax order orderitem);
+
+  # Clean up: remove invoices, orders, parts and customers
+  "SL::DB::Manager::${_}"->delete_all(all => 1) for qw(InvoiceItem Invoice OrderItem Order Customer Part);
+
+  $customer     = SL::DB::Customer->new(
+    name        => 'Test Customer',
+    currency_id => $currency_id,
+    taxzone_id  => $tax_zone->id,
+    %{ $params{customer} }
+  )->save;
+
+  $part = SL::DB::Part->new(
+    partnumber         => 'T4254',
+    description        => 'Fourty-two fifty-four',
+    lastcost           => 222.22,
+    sellprice          => 333.33,
+    buchungsgruppen_id => $buchungsgruppe->id,
+    unit               => $unit->name,
+    %{ $params{part} }
+  )->save;
+  $part->load;
+
+  $order                     = SL::DB::Order->new(
+    customer_id              => $customer->id,
+    currency_id              => $currency_id,
+    taxzone_id               => $tax_zone->id,
+    transaction_description  => '<%period_start_date%>',
+    transdate                => DateTime->from_kivitendo('01.03.2014'),
+    orderitems               => [
+      { parts_id             => $part->id,
+        description          => $part->description,
+        lastcost             => $part->lastcost,
+        sellprice            => $part->sellprice,
+        qty                  => 1,
+        unit                 => $unit->name,
+        %{ $params{orderitem} },
+      },
+    ],
+    periodic_invoices_config => $params{periodic_invoices_config} ? {
+      active                 => 1,
+      ar_chart_id            => $ar_chart->id,
+      %{ $params{periodic_invoices_config} },
+    } : undef,
+    %{ $params{order} },
+  );
+
+  $order->calculate_prices_and_taxes;
+
+  ok($order->save(cascade => 1));
+
+  $::form         = Form->new('');
+  $::form->{year} = 2014;
+  $ctrl           = SL::Controller::FinancialOverview->new;
+
+  $ctrl->get_objects;
+  $ctrl->calculate_one_time_data;
+  $ctrl->calculate_periodic_invoices;
+}
+
+init_common_state();
+
+# ----------------------------------------------------------------------
+# An order without periodic invoices:
+create_sales_order();
+
+is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "no periodic invoices, data for $_")
+  for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_quotations);
+
+is_deeply($ctrl->data->{$_}, { months => [ 0, 0, 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], quarters => [ 333.33, 0, 0, 0 ], year => 333.33 }, "no periodic invoices, data for $_")
+  for qw(sales_orders sales_orders_per_inv);
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q
+create_sales_order(
+  periodic_invoices_config  => {
+    periodicity             => 'm',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2014'),
+  });
+
+is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "periodic conf p=m ovp=y, no invoices, data for $_")
+  for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_quotations);
+
+is_deeply($ctrl->data->{sales_orders},
+          { months => [ 0, 0, 0, 0, 27.7775, 27.7775, 27.7775, 27.7775, 27.7775, 27.7775, 27.7775, 27.7775 ], quarters => [ 0, 55.555, 83.3325, 83.3325 ], year => 222.22 },
+          "periodic conf p=m ovp=y, no invoices, data for sales_orders");
+is_deeply($ctrl->data->{sales_orders_per_inv},
+          { months => [ 0, 0, 333.33, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], quarters => [ 333.33, 0, 0, 0 ], year => 333.33 },
+          "periodic conf p=m ovp=y, no invoices, data for sales_orders_per_inv");
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q, starting in previous year
+create_sales_order(
+  order                     => {
+    transdate               => DateTime->from_kivitendo('01.03.2013'),
+  },
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+  });
+
+is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "periodic conf p=q ovp=y, no invoices, starting previous year, data for $_")
+  for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_quotations);
+
+is_deeply($ctrl->data->{sales_orders},
+          { months => [ 0, 83.3325, 0, 0, 83.3325, 0, 0, 83.3325, 0, 0, 83.3325, 0 ], quarters => [ 83.3325, 83.3325, 83.3325, 83.3325 ], year => 333.33 },
+          "periodic conf p=q ovp=y, no invoices, starting previous year, data for sales_orders");
+is_deeply($ctrl->data->{sales_orders_per_inv},
+          { months => [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], quarters => [ 0, 0, 0, 0 ], year => 0 },
+          "periodic conf p=q ovp=y, no invoices, starting previous year, data for sales_orders_per_inv");
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q, starting in previous year, ending middle of year
+create_sales_order(
+  order                     => {
+    transdate               => DateTime->from_kivitendo('01.03.2013'),
+  },
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2013'),
+    end_date                => DateTime->from_kivitendo('01.09.2014'),
+    terminated              => 1,
+  });
+
+is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "periodic conf p=q ovp=y, no invoices, starting previous year, ending middle of year, data for $_")
+  for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_quotations);
+
+is_deeply($ctrl->data->{sales_orders},
+          { months => [ 0, 83.3325, 0, 0, 83.3325, 0, 0, 83.3325, 0, 0, 0, 0 ], quarters => [ 83.3325, 83.3325, 83.3325, 0 ], year => 249.9975 },
+          "periodic conf p=q ovp=y, no invoices, starting previous year, ending middle of year, data for sales_orders");
+is_deeply($ctrl->data->{sales_orders_per_inv},
+          { months => [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], quarters => [ 0, 0, 0, 0 ], year => 0 },
+          "periodic conf p=q ovp=y, no invoices, starting previous year, ending middle of year, data for sales_orders_per_inv");
+
+# ----------------------------------------------------------------------
+# order_value_periodicity=y, periodicity=q, starting and ending before current
+create_sales_order(
+  order                     => {
+    transdate               => DateTime->from_kivitendo('01.03.2012'),
+  },
+  periodic_invoices_config  => {
+    periodicity             => 'q',
+    order_value_periodicity => 'y',
+    start_date              => DateTime->from_kivitendo('01.05.2012'),
+    end_date                => DateTime->from_kivitendo('01.09.2013'),
+    terminated              => 1,
+  });
+
+is_deeply($ctrl->data->{$_}, { months => [ (0) x 12 ], quarters => [ 0, 0, 0, 0 ], year => 0 }, "periodic conf p=q ovp=y, no invoices, starting and ending before current year, data for $_")
+  for qw(purchase_invoices purchase_orders requests_for_quotation sales_invoices sales_orders sales_orders_per_inv sales_quotations);
+
+done_testing();
index 093cf05..656dc43 100644 (file)
@@ -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">
     </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>