Merge branch 'master' of github.com:kivitendo/kivitendo-erp
authorJan Büren <jan@kivitendo-premium.de>
Fri, 20 Dec 2013 10:52:44 +0000 (11:52 +0100)
committerJan Büren <jan@kivitendo-premium.de>
Fri, 20 Dec 2013 10:52:44 +0000 (11:52 +0100)
30 files changed:
SL/CA.pm
SL/Controller/FinancialControllingReport.pm [new file with mode: 0644]
SL/Controller/FinancialOverview.pm [new file with mode: 0644]
SL/Controller/Project.pm
SL/Controller/ProjectType.pm [new file with mode: 0644]
SL/DB/Buchungsgruppe.pm
SL/DB/Helper/ALL.pm
SL/DB/Helper/Filtered.pm
SL/DB/Helper/Mappings.pm
SL/DB/Manager/Order.pm
SL/DB/Manager/Project.pm
SL/DB/Manager/ProjectType.pm [new file with mode: 0644]
SL/DB/MetaSetup/Project.pm
SL/DB/MetaSetup/ProjectType.pm [new file with mode: 0644]
SL/DB/ProjectType.pm [new file with mode: 0644]
SL/DN.pm
bin/mozilla/io.pl
bin/mozilla/oe.pl
locale/de/all
menus/erp.ini
sql/Pg-upgrade2/project_types.sql [new file with mode: 0644]
t/controllers/helpers/parse_filter.t
templates/webpages/financial_controlling_report/_filter.html [new file with mode: 0644]
templates/webpages/financial_controlling_report/report_bottom.html [new file with mode: 0644]
templates/webpages/financial_controlling_report/report_top.html [new file with mode: 0644]
templates/webpages/financial_overview/report_top.html [new file with mode: 0644]
templates/webpages/project/form.html
templates/webpages/project/search.html
templates/webpages/project_type/form.html [new file with mode: 0755]
templates/webpages/project_type/list.html [new file with mode: 0644]

index ef6e9c3..cee8316 100644 (file)
--- a/SL/CA.pm
+++ b/SL/CA.pm
@@ -1,4 +1,4 @@
-  #=====================================================================
+#=====================================================================
 # LX-Office ERP
 # Copyright (C) 2004
 # Based on SQL-Ledger Version 2.1.9
diff --git a/SL/Controller/FinancialControllingReport.pm b/SL/Controller/FinancialControllingReport.pm
new file mode 100644 (file)
index 0000000..bedc762
--- /dev/null
@@ -0,0 +1,249 @@
+package SL::Controller::FinancialControllingReport;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use List::Util qw(sum);
+
+use SL::DB::Order;
+use SL::DB::ProjectType;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Locale::String;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(project_types) ],
+  'scalar --get_set_init' => [ qw(models) ],
+);
+
+__PACKAGE__->run_before(sub { $::auth->assert('sales_order_edit'); });
+
+my %sort_columns = (
+  ordnumber               => t8('Order'),
+  customer                => t8('Customer'),
+  transaction_description => t8('Transaction description'),
+  globalprojectnumber     => t8('Project'),
+  globalproject_type      => t8('Project Type'),
+  netamount               => t8('Order amount'),
+);
+
+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->calculate_data;
+
+  $self->list_objects;
+}
+
+# private functions
+
+sub prepare_report {
+  my ($self)      = @_;
+
+  my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
+  $self->{report} = $report;
+
+  my @columns     = qw(customer globalprojectnumber globalproject_type ordnumber netamount delivered_amount delivered_amount_p billed_amount billed_amount_p paid_amount paid_amount_p
+                       billable_amount billable_amount_p other_amount);
+  my @sortable    = qw(ordnumber transdate customer netamount globalprojectnumber globalproject_type);
+  $self->{number_columns} = [ qw(netamount billed_amount billed_amount_p delivered_amount delivered_amount_p paid_amount paid_amount_p other_amount billable_amount billable_amount_p) ];
+
+  my %column_defs           = (
+    netamount               => {                                                                                         },
+    billed_amount           => { text     => $::locale->text('Billed amount')                                            },
+    billed_amount_p         => { text     => $::locale->text('%')                                                        },
+    delivered_amount        => { text     => $::locale->text('Delivered amount')                                         },
+    delivered_amount_p      => { text     => $::locale->text('%')                                                        },
+    paid_amount             => { text     => $::locale->text('Paid amount')                                              },
+    paid_amount_p           => { text     => $::locale->text('%')                                                        },
+    billable_amount         => { text     => $::locale->text('Billable amount')                                          },
+    billable_amount_p       => { text     => $::locale->text('%')                                                        },
+    other_amount            => { text     => $::locale->text('Billed extra expenses')                                    },
+    ordnumber               => { obj_link => sub { $self->link_to($_[0])                                              }  },
+    customer                => {      sub => sub { $_[0]->customer->name                                              },
+                                 obj_link => sub { $self->link_to($_[0]->customer)                                    }  },
+    globalprojectnumber     => {      sub => sub { $_[0]->globalproject_id ? $_[0]->globalproject->projectnumber : '' },
+                                 obj_link => sub { $self->link_to($_[0]->globalproject)                               }  },
+    globalproject_type      => { text     => $::locale->text('Project type'),
+                                 sub      => sub { $_[0]->globalproject_id ? $_[0]->globalproject->project_type->description : '' }  },
+  );
+
+  map { $column_defs{$_}->{text} ||= $::locale->text( $self->models->get_sort_spec->{$_}->{title} ) } keys %column_defs;
+  map { $column_defs{$_}->{align} = 'right' } @{ $self->{number_columns} };
+
+  $report->set_options(
+    std_column_visibility => 1,
+    controller_class      => 'FinancialControllingReport',
+    output_format         => 'HTML',
+    top_info_text         => $::locale->text('Financial controlling report for open sales orders'),
+    title                 => $::locale->text('Financial Controlling Report'),
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+  $report->set_columns(%column_defs);
+  $report->set_column_order(@columns);
+  $report->set_export_options(qw(list filter));
+  $report->set_options_from_form;
+  $self->models->disable_plugin('paginated') if $report->{options}{output_format} =~ /^(pdf|csv)$/i;
+  $self->models->finalize;
+  $self->models->set_report_generator_sort_options(report => $report, sortable_columns => \@sortable);
+  $report->set_options(
+    raw_top_info_text     => $self->render('financial_controlling_report/report_top',    { output => 0 }),
+    raw_bottom_info_text  => $self->render('financial_controlling_report/report_bottom', { output => 0 }, models => $self->models),
+  );
+}
+
+sub calculate_data {
+  my ($self) = @_;
+
+  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' ])                  };
+
+    @invoices = (
+      grep({ !$_->storno_id } @invoices),
+      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->{other_amount}      = $billed_amount             - $order->{billed_amount};
+    $order->{billable_amount}   = $order->{delivered_amount} - $order->{billed_amount};
+
+    foreach (qw(delivered billed paid billable)) {
+      $order->{"${_}_amount_p"} = $order->netamount * 1 ? $order->{"${_}_amount"} * 100 / $order->netamount : undef;
+    }
+  }
+}
+
+sub sum_items {
+  my ($self, %params) = @_;
+
+  my %vals;
+
+  foreach my $item (@{ $params{obj}->items }) {
+    my $key  = $item->parts_id;
+    $key    .= ':' . $item->serialnumber if $item->serialnumber;
+
+    $vals{$key}            ||= { parts_id => $item->parts_id, serialnumber => $item->serialnumber, amount => 0, base_qty => 0 };
+    $vals{$key}->{amount}   += $item->qty * $item->sellprice * (1 - $item->discount) / (($item->price_factor * 1) || 1);
+    $vals{$key}->{base_qty} += $item->qty * $item->unit_obj->base_factor;
+  }
+
+  return \%vals;
+}
+
+sub sum_relevant_items {
+  my ($self, %params) = @_;
+
+  $params{order}->{amounts_by_parts_id} ||= $self->sum_items(obj => $params{order});
+  my $sums                                = $self->sum_items(obj => $params{other});
+  my $total                               = 0;
+
+  foreach my $item (grep { $params{order}->{amounts_by_parts_id}->{ $_->{parts_id} } } values %{ $sums }) {
+    my $key = $item->{parts_id};
+
+    if ($params{by_order}) {
+      $key           .= ':' . $item->{serialnumber};
+      my $order_item  = $params{order}->{amounts_by_parts_id}->{ $key } || $params{order}->{amounts_by_parts_id}->{ $item->{parts_id} };
+
+      if ($order_item && $order_item->{base_qty}) {
+        $total += $order_item->{amount} * $item->{base_qty} / $order_item->{base_qty};
+        next;
+      }
+    }
+
+    $total += $item->{amount};
+  }
+
+  return $total;
+}
+
+sub list_objects {
+  my ($self)      = @_;
+  my $modify_data = sub {
+    my ($data) = @_;
+    map { $data->{$_}->{data} = defined $data->{$_}->{data} ? int($data->{$_}->{data}) : ''  } grep {  m/_p$/ } @{ $self->{number_columns} };
+    map { $data->{$_}->{data} = $::form->format_amount(\%::myconfig, $data->{$_}->{data}, 2) } grep { !m/_p$/ } @{ $self->{number_columns} };
+  };
+
+  return $self->report_generator_list_objects(report => $self->{report}, objects => $self->{orders}, data_callback => $modify_data);
+}
+
+sub make_filter_summary {
+  my ($self) = @_;
+
+  my $filter = $::form->{filter} || {};
+  my @filter_strings;
+
+  my @filters = (
+    [ $filter->{"ordnumber:substr::ilike"},                $::locale->text('Number')                                          ],
+    [ $filter->{"transdate:date::ge"},                     $::locale->text('Order Date') . " " . $::locale->text('From Date') ],
+    [ $filter->{"transdate:date::le"},                     $::locale->text('Order Date') . " " . $::locale->text('To Date')   ],
+    [ $filter->{customer}{"name:substr::ilike"},           $::locale->text('Customer')                                        ],
+    [ $filter->{customer}{"customernumber:substr::ilike"}, $::locale->text('Customer Number')                                 ],
+  );
+
+  $self->{filter_summary} = join ', ', @filter_strings;
+}
+
+sub init_models {
+  my ($self) = @_;
+
+  SL::Controller::Helper::GetModels->new(
+    controller   => $self,
+    model        => 'Order',
+    sorted       => {
+      _default     => {
+        by           => 'globalprojectnumber',
+        dir          => 1,
+      },
+      %sort_columns,
+    },
+    query => [
+      SL::DB::Manager::Order->type_filter('sales_order'),
+      '!closed' => 1,
+      or        => [
+        globalproject_id => undef,
+        and              => [
+          'globalproject.active' => 1,
+          'globalproject.valid'  => 1,
+        ]],
+    ],
+    with_objects => [ 'customer', 'globalproject', 'globalproject.project_type' ],
+  );
+}
+
+sub link_to {
+  my ($self, $object, %params) = @_;
+
+  return unless $object;
+  my $action = $params{action} || 'edit';
+
+  if ($object->isa('SL::DB::Order')) {
+    my $type = $object->type;
+    my $id   = $object->id;
+
+    return "oe.pl?action=$action&type=$type&vc=customer&id=$id";
+  }
+  if ($object->isa('SL::DB::Customer')) {
+    my $id     = $object->id;
+    return "ct.pl?action=$action&id=$id&db=customer";
+  }
+  if ($object->isa('SL::DB::Project')) {
+    my $id     = $object->id;
+    return "controller.pl?action=Project/$action&id=$id";
+  }
+}
+
+1;
diff --git a/SL/Controller/FinancialOverview.pm b/SL/Controller/FinancialOverview.pm
new file mode 100644 (file)
index 0000000..38d2fc6
--- /dev/null
@@ -0,0 +1,165 @@
+package SL::Controller::FinancialOverview;
+
+use strict;
+use parent qw(SL::Controller::Base);
+
+use List::MoreUtils qw(none);
+
+use SL::DB::Invoice;
+use SL::DB::Order;
+use SL::DB::PurchaseInvoice;
+use SL::Controller::Helper::ReportGenerator;
+use SL::Locale::String;
+
+use Rose::Object::MakeMethods::Generic (
+  scalar => [ qw(report number_columns year current_year types objects data subtotals_per_quarter) ],
+);
+
+__PACKAGE__->run_before(sub { $::auth->assert('report'); });
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->subtotals_per_quarter($::form->{subtotals_per_quarter});
+
+  $self->get_objects;
+  $self->calculate_data;
+  $self->prepare_report;
+  $self->list_data;
+}
+
+# private functions
+
+sub prepare_report {
+  my ($self)      = @_;
+
+  $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);
+
+  $self->number_columns([ grep { !m/^(?:month|year|quarter)$/ } @columns ]);
+
+  my %column_defs          = (
+    month                  => { text => t8('Month')                  },
+    year                   => { text => t8('Year')                   },
+    quarter                => { text => t8('Quarter')                },
+    sales_quotations       => { text => t8('Sales Quotations')       },
+    sales_orders           => { text => t8('Sales Orders')           },
+    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;
+
+  $self->report->set_options(
+    std_column_visibility => 1,
+    controller_class      => 'FinancialOverview',
+    output_format         => 'HTML',
+    raw_top_info_text     => $self->render('financial_overview/report_top', { output => 0 }, YEARS_TO_LIST => [ reverse(2000..$self->current_year) ]),
+    title                 => t8('Financial overview for #1', $self->year),
+    allow_pdf_export      => 1,
+    allow_csv_export      => 1,
+  );
+  $self->report->set_columns(%column_defs);
+  $self->report->set_column_order(@columns);
+  $self->report->set_export_options(qw(list year subtotals_per_quarter));
+  $self->report->set_options_from_form;
+}
+
+sub get_objects {
+  my ($self) = @_;
+
+  $self->current_year(DateTime->today->year);
+  $self->year($::form->{year} || DateTime->today->year);
+
+  my $start       = DateTime->new(year => $self->year, month => 1, day => 1);
+  my $end         = DateTime->new(year => $self->year, month => 12, day => 31);
+
+  my @date_filter = (and => [ transdate => { ge => $start }, transdate => { le => $end } ]);
+
+  $self->objects({
+    sales_quotations       => SL::DB::Manager::Order->get_all(          where => [ and => [ @date_filter, SL::DB::Manager::Order->type_filter('sales_quotation')   ]]),
+    sales_orders           => SL::DB::Manager::Order->get_all(          where => [ and => [ @date_filter, SL::DB::Manager::Order->type_filter('sales_order')       ]]),
+    requests_for_quotation => SL::DB::Manager::Order->get_all(          where => [ and => [ @date_filter, SL::DB::Manager::Order->type_filter('request_quotation') ]]),
+    purchase_orders        => SL::DB::Manager::Order->get_all(          where => [ and => [ @date_filter, SL::DB::Manager::Order->type_filter('purchase_order')    ]]),
+    sales_invoices         => SL::DB::Manager::Invoice->get_all(        where => \@date_filter),
+    purchase_invoices      => SL::DB::Manager::PurchaseInvoice->get_all(where => \@date_filter),
+  });
+}
+
+sub calculate_data {
+  my ($self) = @_;
+
+  $self->types([ qw(sales_quotations sales_orders sales_invoices requests_for_quotation purchase_orders purchase_invoices) ]);
+
+  my %data  = (
+    year    => [ ($self->year) x 12                   ],
+    month   => [ (1..12)                              ],
+    quarter => [ (1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4) ],
+    map {
+      $_ => {
+        months   => [ (0) x 12 ],
+        quarters => [ (0) x  4 ],
+        year     => 0,
+      }
+    } @{ $self->types },
+  );
+
+  foreach my $type (keys %{ $self->objects }) {
+    foreach my $object (@{ $self->objects->{ $type } }) {
+      my $month                              = $object->transdate->month - 1;
+      my $tdata                              = $data{$type};
+
+      $tdata->{months}->[$month]            += $object->netamount;
+      $tdata->{quarters}->[int($month / 3)] += $object->netamount;
+      $tdata->{year}                        += $object->netamount;
+    }
+  }
+
+  $self->data(\%data);
+}
+
+sub list_data {
+  my ($self)           = @_;
+
+  my @visible_columns  = $self->report->get_visible_columns;
+  my @type_columns     = @{ $self->types };
+  my @non_type_columns = grep { my $c = $_; none { $c eq $_ } @type_columns } @visible_columns;
+
+  for my $month (1..12) {
+    my %data  = (
+      map({ ($_ => { data => $self->data->{$_}->[$month - 1]                                                    }) } @non_type_columns),
+      map({ ($_ => { data => $::form->format_amount(\%::myconfig, $self->data->{$_}->{months}->[$month - 1], 2) }) } @type_columns    ),
+    );
+
+    $self->report->add_data(\%data);
+
+    if ($self->subtotals_per_quarter && (($month % 3) == 0)) {
+      my %subtotal =  (
+        year       => { data => $self->year },
+        month      => { data => $::locale->text('Total') },
+        map { ($_ => { data => $::form->format_amount(\%::myconfig, $self->data->{$_}->{quarters}->[int(($month - 1) / 3)], 2) }) } @type_columns,
+      );
+
+      $subtotal{$_}->{class} = 'listsubtotal' for @visible_columns;
+
+      $self->report->add_data(\%subtotal);
+    }
+  }
+
+  my %data  =  (
+    year    => { data => $self->year },
+    quarter => { data => $::locale->text('Total') },
+    map { ($_ => { data => $::form->format_amount(\%::myconfig, $self->data->{$_}->{year}, 2) }) } @type_columns,
+  );
+
+  $data{$_}->{class} = 'listtotal' for @visible_columns;
+
+  $self->report->add_data(\%data);
+
+  return $self->report->generate_with_headers;
+}
+
+1;
index a5793b8..23e5228 100644 (file)
@@ -16,6 +16,7 @@ use SL::DB::Invoice;
 use SL::DB::Order;
 use SL::DB::Project;
 use SL::DB::PurchaseInvoice;
+use SL::DB::ProjectType;
 use SL::Helper::Flash;
 use SL::Locale::String;
 
@@ -26,7 +27,7 @@ use Rose::Object::MakeMethods::Generic
 );
 
 __PACKAGE__->run_before('check_auth');
-__PACKAGE__->run_before('load_project', only => [ qw(edit update destroy) ]);
+__PACKAGE__->run_before('load_project',       only => [ qw(edit update destroy) ]);
 
 #
 # actions
@@ -37,7 +38,8 @@ sub action_search {
 
   my %params;
 
-  $params{CUSTOM_VARIABLES} = CVar->get_configs(module => 'Projects');
+  $params{ALL_PROJECT_TYPES} = SL::DB::Manager::ProjectType->get_all_sorted;
+  $params{CUSTOM_VARIABLES}  = CVar->get_configs(module => 'Projects');
   ($params{CUSTOM_VARIABLES_FILTER_CODE}, $params{CUSTOM_VARIABLES_INCLUSION_CODE})
     = CVar->render_search_options(variables      => $params{CUSTOM_VARIABLES},
                                   include_prefix => 'l_',
@@ -121,8 +123,9 @@ sub check_auth {
 sub display_form {
   my ($self, %params) = @_;
 
-  $params{ALL_CUSTOMERS}    = SL::DB::Manager::Customer->get_all_sorted(where => [ or => [ obsolete => 0, obsolete => undef, id => $self->project->customer_id ]]);
-  $params{CUSTOM_VARIABLES} = CVar->get_custom_variables(module => 'Projects', trans_id => $self->project->id);
+  $params{ALL_CUSTOMERS}     = SL::DB::Manager::Customer->get_all_sorted(where => [ or => [ obsolete => 0, obsolete => undef, id => $self->project->customer_id ]]);
+  $params{ALL_PROJECT_TYPES} = SL::DB::Manager::ProjectType->get_all_sorted;
+  $params{CUSTOM_VARIABLES}  = CVar->get_custom_variables(module => 'Projects', trans_id => $self->project->id);
   CVar->render_inputs(variables => $params{CUSTOM_VARIABLES}) if @{ $params{CUSTOM_VARIABLES} };
 
   $self->render('project/form', %params);
@@ -171,7 +174,7 @@ sub setup_db_args_from_filter {
   $self->{filter} = {};
   my %args = parse_filter(
     $self->_pre_parse_filter($::form->{filter}, $self->{filter}),
-    with_objects => [ 'customer' ],
+    with_objects => [ 'customer', 'project_type' ],
     launder_to   => $self->{filter},
   );
 
@@ -186,13 +189,13 @@ sub prepare_report {
   my $report      = SL::ReportGenerator->new(\%::myconfig, $::form);
   $self->{report} = $report;
 
-  my @columns     = qw(projectnumber description customer active valid type);
-  my @sortable    = qw(projectnumber description customer              type);
+  my @columns     = qw(projectnumber description customer active valid project_type);
+  my @sortable    = qw(projectnumber description customer              project_type);
 
   my %column_defs = (
     projectnumber => { obj_link => sub { $self->url_for(action => 'edit', id => $_[0]->id, callback => $callback) } },
     description   => { obj_link => sub { $self->url_for(action => 'edit', id => $_[0]->id, callback => $callback) } },
-    type          => { },
+    project_type  => { sub  => sub { $_[0]->project_type->description } },
     customer      => { sub  => sub { $_[0]->customer ? $_[0]->customer->name     : '' } },
     active        => { sub  => sub { $_[0]->active   ? $::locale->text('Active') : $::locale->text('Inactive') },
                        text => $::locale->text('Active') },
@@ -235,7 +238,7 @@ sub init_models {
       customer      => t8('Customer'),
       description   => t8('Description'),
       projectnumber => t8('Project Number'),
-      type          => t8('Type'),
+      project_type  => t8('Project Type'),
     },
     with_objects => [ 'customer' ],
   );
diff --git a/SL/Controller/ProjectType.pm b/SL/Controller/ProjectType.pm
new file mode 100644 (file)
index 0000000..e986924
--- /dev/null
@@ -0,0 +1,112 @@
+package SL::Controller::ProjectType;
+
+use strict;
+
+use parent qw(SL::Controller::Base);
+
+use SL::DB::ProjectType;
+use SL::Helper::Flash;
+
+use Rose::Object::MakeMethods::Generic
+(
+ scalar => [ qw(project_type) ],
+);
+
+__PACKAGE__->run_before('check_auth');
+__PACKAGE__->run_before('load_project_type', only => [ qw(edit update destroy) ]);
+
+#
+# actions
+#
+
+sub action_list {
+  my ($self) = @_;
+
+  $self->render('project_type/list',
+                title         => $::locale->text('Project Types'),
+                PROJECT_TYPES => SL::DB::Manager::ProjectType->get_all_sorted);
+}
+
+sub action_new {
+  my ($self) = @_;
+
+  $self->{project_type} = SL::DB::ProjectType->new;
+  $self->render('project_type/form', title => $::locale->text('Create a new project type'));
+}
+
+sub action_edit {
+  my ($self) = @_;
+  $self->render('project_type/form', title => $::locale->text('Edit project type'));
+}
+
+sub action_create {
+  my ($self) = @_;
+
+  $self->{project_type} = SL::DB::ProjectType->new;
+  $self->create_or_update;
+}
+
+sub action_update {
+  my ($self) = @_;
+  $self->create_or_update;
+}
+
+sub action_destroy {
+  my ($self) = @_;
+
+  if (eval { $self->{project_type}->delete; 1; }) {
+    flash_later('info',  $::locale->text('The project type has been deleted.'));
+  } else {
+    flash_later('error', $::locale->text('The project type is in use and cannot be deleted.'));
+  }
+
+  $self->redirect_to(action => 'list');
+}
+
+sub action_reorder {
+  my ($self) = @_;
+
+  SL::DB::ProjectType->reorder_list(@{ $::form->{project_type_id} || [] });
+
+  $self->render(\'', { type => 'json' });
+}
+
+#
+# filters
+#
+
+sub check_auth {
+  $::auth->assert('config');
+}
+
+#
+# helpers
+#
+
+sub create_or_update {
+  my $self   = shift;
+  my $is_new = !$self->{project_type}->id;
+  my $params = delete($::form->{project_type}) || { };
+
+  $self->{project_type}->assign_attributes(%{ $params });
+
+  my @errors = $self->{project_type}->validate;
+
+  if (@errors) {
+    flash('error', @errors);
+    $self->render('project_type/form', title => $is_new ? $::locale->text('Create a new project type') : $::locale->text('Edit project type'));
+    return;
+  }
+
+  $self->{project_type}->save;
+
+  flash_later('info', $is_new ? $::locale->text('The project type has been created.') : $::locale->text('The project type has been saved.'));
+  $self->redirect_to(action => 'list');
+}
+
+sub load_project_type {
+  my ($self) = @_;
+  $self->{project_type} = SL::DB::ProjectType->new(id => $::form->{id})->load;
+}
+
+1;
index 1045ac1..5f4d831 100644 (file)
@@ -9,7 +9,7 @@ __PACKAGE__->meta->add_relationship(
   inventory_account => {
     type          => 'many to one',
     class         => 'SL::DB::Chart',
-    column_map    => { income_accno_id_0 => 'id' },
+    column_map    => { inventory_accno_id => 'id' },
   },
   income_account_0 => {
     type         => 'many to one',
index 4463567..498583f 100644 (file)
@@ -70,6 +70,7 @@ use SL::DB::Pricegroup;
 use SL::DB::Price;
 use SL::DB::Printer;
 use SL::DB::Project;
+use SL::DB::ProjectType;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordLink;
 use SL::DB::SchemaInfo;
index 15e0985..e95c01f 100644 (file)
@@ -14,7 +14,7 @@ sub filter {
 
   my $filters = _get_filters($class);
 
-  return ($key, $value) unless $filters->{$key};
+  return ($prefix . $key, $value) unless $filters->{$key};
 
   return $filters->{$key}->($key, $value, $prefix);
 }
index 35d601d..633a067 100644 (file)
@@ -151,6 +151,7 @@ my %kivitendo_package_names = (
   pricegroup                     => 'pricegroup',
   printers                       => 'printer',
   project                        => 'project',
+  project_types                  => 'ProjectType',
   record_links                   => 'record_link',
   sepa_export                    => 'sepa_export',
   sepa_export_items              => 'sepa_export_item',
index 69aa266..e03386d 100644 (file)
@@ -38,6 +38,13 @@ sub _sort_spec {
       customer                => 'customer.name',
       vendor                  => 'vendor.name',
       globalprojectnumber     => 'lower(globalproject.projectnumber)',
+
+      # Bug in Rose::DB::Object: the next should be
+      # "globalproject.project_type.description". This workaround will
+      # only work if no other table with "project_type" is visible in
+      # the current query
+      globalproject_type      => 'lower(project_type.description)',
+
       map { ( $_ => "lower(oe.$_)" ) } qw(ordnumber quonumber cusordnumber shippingpoint shipvia notes intnotes transaction_description),
     });
 }
index 6c5563f..f6ff127 100644 (file)
@@ -43,10 +43,11 @@ our @tables_with_project_id_cols = qw(acc_trans ap ar delivery_order_items deliv
 
 sub _sort_spec {
   return (
-    default    => [ 'projectnumber', 1 ],
-    columns    => {
-      SIMPLE   => 'ALL',
-      customer => 'customer.name',
+    default        => [ 'projectnumber', 1 ],
+    columns        => {
+      SIMPLE       => 'ALL',
+      customer     => 'customer.name',
+      project_type => 'project_type.description',
     });
 }
 
diff --git a/SL/DB/Manager/ProjectType.pm b/SL/DB/Manager/ProjectType.pm
new file mode 100644 (file)
index 0000000..3fa990f
--- /dev/null
@@ -0,0 +1,51 @@
+package SL::DB::Manager::ProjectType;
+
+use strict;
+
+use parent qw(SL::DB::Helper::Manager);
+
+use SL::DB::Helper::Paginated;
+use SL::DB::Helper::Sorted;
+
+sub object_class { 'SL::DB::ProjectType' }
+
+__PACKAGE__->make_manager_methods;
+
+sub _sort_spec {
+  return (
+    default       => [ 'position', 1 ],
+    columns       => {
+      SIMPLE      => 'ALL',
+      description => 'lower(project_types.description)',
+    });
+}
+
+1;
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::DB::Manager::ProjectType - Manager for models for the 'project_types' table
+
+=head1 SYNOPSIS
+
+This is a standard Rose::DB::Manager based model manager and can be
+used as such.
+
+=head1 FUNCTIONS
+
+None yet.
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>
+
+=cut
index 4805568..5131535 100644 (file)
@@ -9,15 +9,15 @@ use base qw(SL::DB::Object);
 __PACKAGE__->meta->table('project');
 
 __PACKAGE__->meta->columns(
-  active        => { type => 'boolean', default => 'true' },
-  customer_id   => { type => 'integer' },
-  description   => { type => 'text' },
-  id            => { type => 'integer', not_null => 1, sequence => 'id' },
-  itime         => { type => 'timestamp', default => 'now()' },
-  mtime         => { type => 'timestamp' },
-  projectnumber => { type => 'text' },
-  type          => { type => 'text' },
-  valid         => { type => 'boolean', default => 'true' },
+  active          => { type => 'boolean', default => 'true' },
+  customer_id     => { type => 'integer' },
+  description     => { type => 'text' },
+  id              => { type => 'integer', not_null => 1, sequence => 'id' },
+  itime           => { type => 'timestamp', default => 'now()' },
+  mtime           => { type => 'timestamp' },
+  project_type_id => { type => 'integer', not_null => 1 },
+  projectnumber   => { type => 'text' },
+  valid           => { type => 'boolean', default => 'true' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
@@ -31,6 +31,11 @@ __PACKAGE__->meta->foreign_keys(
     class       => 'SL::DB::Customer',
     key_columns => { customer_id => 'id' },
   },
+
+  project_type => {
+    class       => 'SL::DB::ProjectType',
+    key_columns => { project_type_id => 'id' },
+  },
 );
 
 1;
diff --git a/SL/DB/MetaSetup/ProjectType.pm b/SL/DB/MetaSetup/ProjectType.pm
new file mode 100644 (file)
index 0000000..3d452cc
--- /dev/null
@@ -0,0 +1,20 @@
+# This file has been auto-generated. Do not modify it; it will be overwritten
+# by rose_auto_create_model.pl automatically.
+package SL::DB::ProjectType;
+
+use strict;
+
+use base qw(SL::DB::Object);
+
+__PACKAGE__->meta->table('project_types');
+
+__PACKAGE__->meta->columns(
+  description => { type => 'text' },
+  id          => { type => 'serial', not_null => 1 },
+  position    => { type => 'integer', not_null => 1 },
+);
+
+__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+
+1;
+;
diff --git a/SL/DB/ProjectType.pm b/SL/DB/ProjectType.pm
new file mode 100644 (file)
index 0000000..6fec148
--- /dev/null
@@ -0,0 +1,29 @@
+package SL::DB::ProjectType;
+
+use strict;
+
+use SL::DB::MetaSetup::ProjectType;
+use SL::DB::Manager::ProjectType;
+
+use SL::DB::Helper::ActsAsList;
+
+__PACKAGE__->meta->add_relationship(
+  projects => {
+    type         => 'many to one',
+    class        => 'SL::DB::Project',
+    column_map   => { id => 'project_type_id' },
+  },
+);
+
+__PACKAGE__->meta->initialize;
+
+sub validate {
+  my ($self) = @_;
+
+  my @errors;
+  push @errors, $::locale->text('The description is missing.') if !$self->description;
+
+  return @errors;
+}
+
+1;
index b64e48c..c38733e 100644 (file)
--- a/SL/DN.pm
+++ b/SL/DN.pm
@@ -751,6 +751,16 @@ sub print_dunning {
 
   $dunning_id =~ s|[^\d]||g;
 
+  my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
+  if ($form->{"language_id"}) {
+    ($language_tc, $output_numberformat, $output_dateformat, $output_longdates) =
+      AM->get_language_details($myconfig, $form, $form->{language_id});
+  } else {
+    $output_dateformat = $myconfig->{dateformat};
+    $output_numberformat = $myconfig->{numberformat};
+    $output_longdates = 1;
+  }
+
   my $query =
     qq|SELECT
          da.fee, da.interest,
@@ -837,6 +847,17 @@ sub print_dunning {
   $form->{total_open_amount} = $form->format_amount($myconfig, $form->round_amount($ref->{total_open_amount}, 2), 2);
   $form->{total_amount}      = $form->format_amount($myconfig, $form->round_amount($ref->{fee} + $ref->{total_interest} + $ref->{total_open_amount}, 2), 2);
 
+  $::form->format_dates($output_dateformat, $output_longdates,
+    qw(dn_dunning_date dn_dunning_duedate dn_transdate dn_duedate
+          dunning_date    dunning_duedate    transdate    duedate)
+  );
+  $::form->reformat_numbers($output_numberformat, 2, qw(
+    dn_amount dn_netamount dn_paid dn_open_amount dn_fee dn_interest dn_linetotal
+       amount    netamount    paid    open_amount    fee    interest    linetotal
+    total_interest total_open_interest total_amount total_open_amount
+  ));
+  $::form->reformat_numbers($output_numberformat, undef, qw(interest_rate));
+
   $self->set_customer_cvars($myconfig, $form);
   $self->set_template_options($myconfig, $form);
 
index c1bc770..56a0354 100644 (file)
@@ -1920,6 +1920,13 @@ sub _remove_billed_or_delivered_rows {
   my @fields = map { s/_1$//; $_ } grep { m/_1$/ } keys %{ $::form };
   my @new_rows;
 
+  my $make_key = sub {
+    my ($row) = @_;
+    return $::form->{"id_${row}"} unless $::form->{"serialnumber_${row}"};
+    my $key = $::form->{"id_${row}"} . ':' . $::form->{"serialnumber_${row}"};
+    return exists $params{quantities}->{$key} ? $key : $::form->{"id_${row}"};
+  };
+
   my $removed_rows = 0;
   my $row          = 0;
   while ($row < $::form->{rowcount}) {
@@ -1929,8 +1936,9 @@ sub _remove_billed_or_delivered_rows {
     my $parts_id                      = $::form->{"id_$row"};
     my $base_qty                      = $::form->parse_amount(\%::myconfig, $::form->{"qty_$row"}) * SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor;
 
-    my $sub_qty                       = min($base_qty, $params{quantities}->{$parts_id});
-    $params{quantities}->{$parts_id} -= $sub_qty;
+    my $key                           = $make_key->($row);
+    my $sub_qty                       = min($base_qty, $params{quantities}->{$key});
+    $params{quantities}->{$key}      -= $sub_qty;
 
     if (!$sub_qty || ($sub_qty != $base_qty)) {
       $::form->{"qty_${row}"} = $::form->format_amount(\%::myconfig, ($base_qty - $sub_qty) / SL::DB::Manager::Unit->find_by(name => $::form->{"unit_$row"})->base_factor);
index 883507a..6001c87 100644 (file)
@@ -2068,7 +2068,9 @@ sub _oe_remove_delivered_or_billed_rows {
     next if $ord_quot->is_sales != $record->is_sales;
 
     foreach my $item (@{ $record->items }) {
-      $handled_base_qtys{ $item->parts_id } += $item->qty * $item->unit_obj->base_factor;
+      my $key  = $item->parts_id;
+      $key    .= ':' . $item->serialnumber if $item->serialnumber;
+      $handled_base_qtys{$key} += $item->qty * $item->unit_obj->base_factor;
     }
   }
 
index 175623e..710a92b 100755 (executable)
@@ -19,6 +19,9 @@ $self->{texts} = {
   '#1 of #2 importable objects were imported.' => '#1 von #2 importierbaren Objekten wurden importiert.',
   '#1 prices were updated.'     => '#1 Preise wurden aktualisiert.',
   '(recommended) Insert the used currencies in the system. You can simply change the name of the currencies by editing the textfields above. Do not use a name of a currency that is already in use.' => '(empfohlen) Fügen Sie die verwaisten Währungen in Ihr System ein. Sie können den Namen der Währung einfach ändern, indem Sie die Felder oben bearbeiten. Benutzen Sie keine Namen von Währungen, die Sie bereits benutzen.',
+  '%'                           => '%',
+  '* there are restrictions for the perpetual method, look at chapter "Bemerkungen zu Bestandsmethode"  in' => ' für die Bestandsmethode gibt es Einschränkungen, siehe Kapitel "Bemerkungen zu Bestandsmethode"  in',
+  '*) Since version 2.7 these parameters ares set in the client database and not in the configuration file, details in chapter:' => '*) Seit 2.7 werden Gewinnermittlungsart, Versteuerungsart und Warenbuchungsmethode in der Mandanten-DB gesteuert und nicht mehr in der Konfigurationsdatei, Umstellungs-Details:',
   '*/'                          => '*/',
   ', if set'                    => ', falls gesetzt',
   '---please select---'         => '---bitte auswählen---',
@@ -298,6 +301,9 @@ $self->{texts} = {
   'Best Before'                 => 'Mindesthaltbarkeit',
   'Bestandskonto'               => 'Bestandskonto',
   'Bilanz'                      => 'Bilanz',
+  'Billable amount'             => 'Abrechenbarer Betrag',
+  'Billed amount'               => 'Abgerechneter Betrag',
+  'Billed extra expenses'       => 'Abgerechnete Nebenkosten',
   'Billing Address'             => 'Rechnungsadresse',
   'Billing/shipping address (city)' => 'Rechnungsadresse (Stadt)',
   'Billing/shipping address (country)' => 'Rechnungsadresse (Land)',
@@ -402,7 +408,7 @@ $self->{texts} = {
   'Cannot save order!'          => 'Auftrag kann nicht gespeichert werden!',
   'Cannot save preferences!'    => 'Einstellungen können nicht gespeichert werden!',
   'Cannot save quotation!'      => 'Angebot kann nicht gespeichert werden!',
-  'Cannot storno invoice for a closed period!' => 'Das Rechnungsdatum der zu stornierenden Rechnung fällt in einen abgeschlossen Zeitraum!',
+  'Cannot storno invoice for a closed period!' => 'Das Rechnungsdatum der zu stornierenden Rechnung fällt in einen abgeschlossenen Zeitraum!',
   'Cannot storno storno invoice!' => 'Kann eine Stornorechnung nicht stornieren',
   'Cannot transfer. <br> Reason:<br>#1' => 'Kann nicht auslagern. <br>Grund:<br>#1',
   'Carry over shipping address' => 'Lieferadresse &uuml;bernehmen',
@@ -510,8 +516,10 @@ $self->{texts} = {
   'Create a new payment term'   => 'Neue Zahlungsbedingungen anlegen',
   'Create a new printer'        => 'Einen neuen Drucker anlegen',
   'Create a new project'        => 'Neues Projekt anlegen',
+  'Create a new project type'   => 'Einen neuen Projekttypen anlegen',
   'Create a new user'           => 'Einen neuen Benutzer anlegen',
   'Create a new user group'     => 'Eine neue Benutzergruppe erfassen',
+  'Create a standard group'     => 'Eine Standard-Benutzergruppe anlegen',
   'Create and edit RFQs'        => 'Lieferantenanfragen erfassen und bearbeiten',
   'Create and edit dunnings'    => 'Mahnungen erfassen und bearbeiten',
   'Create and edit invoices and credit notes' => 'Rechnungen und Gutschriften erfassen und bearbeiten',
@@ -538,6 +546,7 @@ $self->{texts} = {
   'Create new department'       => 'Neue Abteilung erfassen',
   'Create new payment term'     => 'Neue Zahlungsbedingung anlegen',
   'Create new templates from master templates' => 'Neue Druckvorlagen aus Vorlagensatz erstellen',
+  'Create new project type'     => 'Neuen Projekttypen anlegen',
   'Create tables'               => 'Tabellen anlegen',
   'Created by'                  => 'Erstellt von',
   'Created for'                 => 'Erstellt f&uuml;r',
@@ -569,6 +578,7 @@ $self->{texts} = {
   'Current profile'             => 'Aktuelles Profil',
   'Current status'              => 'Aktueller Status',
   'Current value:'              => 'Aktueller Wert:',
+  'Current year'                => 'Aktuelles Jahr',
   'Custom Variables'            => 'Benutzerdefinierte Variablen',
   'Custom variables for module' => 'Benutzerdefinierte Variablen für Modul',
   'Customer'                    => 'Kunde',
@@ -677,6 +687,7 @@ $self->{texts} = {
   'Delete transaction'          => 'Buchung löschen',
   'Deleted'                     => 'Gelöscht',
   'Delivered'                   => 'Geliefert',
+  'Delivered amount'            => 'Gelieferter Betrag',
   'Delivery Date'               => 'Lieferdatum',
   'Delivery Order'              => 'Lieferschein',
   'Delivery Order Date'         => 'Lieferscheindatum',
@@ -844,6 +855,7 @@ $self->{texts} = {
   'Edit prices and discount (if not used, textfield is ONLY set readonly)' => 'Preise und Rabatt in Formularen frei anpassen (falls deaktiviert, wird allerdings NUR das textfield auf READONLY gesetzt / kann je nach Browserversion und technischen Fähigkeiten des Anwenders noch umgangen werden)',
   'Edit project'                => 'Projekt bearbeiten',
   'Edit project #1'             => 'Projekt #1 bearbeiten',
+  'Edit project type'           => 'Projekttypen bearbeiten',
   'Edit templates'              => 'Vorlagen bearbeiten',
   'Edit the Delivery Order'     => 'Lieferschein bearbeiten',
   'Edit the configuration for periodic invoices' => 'Konfiguration für wiederkehrende Rechnungen bearbeiten',
@@ -964,6 +976,11 @@ $self->{texts} = {
   'Filter for customer variables' => 'Filter für benutzerdefinierte Kundenvariablen',
   'Filter for item variables'   => 'Filter für benutzerdefinierte Artikelvariablen',
   'Filter parts'                => 'Artikel filtern',
+  'Financial Controlling'       => 'Finanzcontrolling',
+  'Financial Controlling Report' => 'Finanzcontrollingbericht',
+  'Financial Overview'          => 'Finanzübersicht',
+  'Financial controlling report for open sales orders' => 'Finanzcontrollingbericht für offene Aufträge',
+  'Financial overview for #1'   => 'Finanzübersicht für #1',
   'Finish'                      => 'Abschlie&szlig;en',
   'First 20 Lines'              => 'Nur erste 20 Datensätze',
   'Fix transaction'             => 'Buchung korrigieren',
@@ -978,6 +995,7 @@ $self->{texts} = {
   'Follow-Up saved.'            => 'Wiedervorlage gespeichert.',
   'Follow-Ups'                  => 'Wiedervorlagen',
   'Follow-up for'               => 'Wiedervorlage für',
+  'Following year'              => 'Folgendes Jahr',
   'Font'                        => 'Schriftart',
   'Font size'                   => 'Schriftgr&ouml;&szlig;e',
   'For AP transactions it will replace the sales taxkeys with input taxkeys with the same tax rate.' => 'Bei Kreditorenbuchungen werden die Umsatzsteuer-Steuerschlüssel durch Vorsteuer-Steuerschlüssel mit demselben Steuersatz ersetzt.',
@@ -1380,6 +1398,7 @@ $self->{texts} = {
   'No print templates have been created for this client yet. Please do so in the client configuration.' => 'Für diesen Mandanten wurden noch keine Druckvorlagen angelegt. Bitte holen Sie dies in der Mandantenkonfiguration nach.',
   'No printers have been created yet.' => 'Es wurden noch keine Drucker angelegt.',
   'No problems were recognized.' => 'Es wurden keine Probleme gefunden.',
+  'No project type has been created yet.' => 'Es wurden noch keine Projekttypen angelegt.',
   'No report with id #1'        => 'Es gibt keinen Report mit der Id #1',
   'No shipto selected to delete' => 'Keine Lieferadresse zum Löschen ausgewählt',
   'No summary account'          => 'Kein Sammelkonto',
@@ -1454,6 +1473,7 @@ $self->{texts} = {
   'Order Date missing!'         => 'Auftragsdatum fehlt!',
   'Order Number'                => 'Auftragsnummer',
   'Order Number missing!'       => 'Auftragsnummer fehlt!',
+  'Order amount'                => 'Auftragswert',
   'Order deleted!'              => 'Auftrag gelöscht!',
   'Order/Item row name'         => 'Name der Auftrag-/Positions-Zeilen',
   'OrderItem'                   => 'Position',
@@ -1487,6 +1507,7 @@ $self->{texts} = {
   'Page'                        => 'Seite',
   'Page #1/#2'                  => 'Seite #1/#2',
   'Paid'                        => 'bezahlt',
+  'Paid amount'                 => 'Bezahlter Betrag',
   'Part'                        => 'Ware',
   'Part (database ID)'          => 'Artikel (Datenbank-ID)',
   'Part Description'            => 'Artikelbeschreibung',
@@ -1629,6 +1650,7 @@ $self->{texts} = {
   'Printer Management'          => 'Druckeradministration',
   'Printer management'          => 'Druckerverwaltung',
   'Printing ... '               => 'Es wird gedruckt.',
+  'Prior year'                  => 'Vorheriges Jahr',
   'Private E-mail'              => 'Private E-Mail',
   'Private Phone'               => 'Privates Tel.',
   'Problem'                     => 'Problem',
@@ -1645,6 +1667,9 @@ $self->{texts} = {
   'Project Number'              => 'Projektnummer',
   'Project Numbers'             => 'Projektnummern',
   'Project Transactions'        => 'Projektbuchungen',
+  'Project Type'                => 'Projekttyp',
+  'Project Types'               => 'Projekttypen',
+  'Project type'                => 'Projekttyp',
   'Projects'                    => 'Projekte',
   'Projecttransactions'         => 'Projektbuchungen',
   'Prozentual/Absolut'          => 'Prozentual/Absolut',
@@ -1737,6 +1762,7 @@ $self->{texts} = {
   'Requested execution date'    => 'Gewünschtes Ausführungsdatum',
   'Requested execution date from' => 'Gewünschtes Ausführungsdatum von',
   'Requested execution date to' => 'Gewünschtes Ausführungsdatum bis',
+  'Requests for Quotation'      => 'Preisanfragen',
   'Required by'                 => 'Lieferdatum',
   'Reset'                       => 'Zurücksetzen',
   'Result'                      => 'Ergebnis',
@@ -1968,6 +1994,7 @@ $self->{texts} = {
   'Subject:'                    => 'Betreff:',
   'Subtotal'                    => 'Zwischensumme',
   'Subtotal cannot distinguish betweens record types. Only one of the selected record types will be displayed: #1' => 'Zwischensummen können nicht zwischen den einzelnen Belegen unterscheiden, es wird nur "#1" angezeigt',
+  'Subtotals per quarter'       => 'Zwischensummen pro Quartal',
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => 'Solche Einträge sind aber nicht DATEV-exportiertbar und müssen ebenfalls korrigiert werden.',
   'Sum Credit'                  => 'Summe Haben',
   'Sum Debit'                   => 'Summe Soll',
@@ -2191,6 +2218,10 @@ $self->{texts} = {
   'The project is in use and cannot be deleted.' => 'Das Projekt ist in Verwendung und kann nicht gelöscht werden.',
   'The project number is already in use.' => 'Die Projektnummer wird bereits verwendet.',
   'The project number is missing.' => 'Die Projektnummer fehlt.',
+  'The project type has been created.' => 'Der Projekttyp wurde angelegt.',
+  'The project type has been deleted.' => 'Der Projekttyp wurde gelöscht.',
+  'The project type has been saved.' => 'Der Projekttyp wurde gespeichert.',
+  'The project type is in use and cannot be deleted.' => 'Der Projekttyp wird verwendet und kann nicht gelöscht werden.',
   'The second reason is that kivitendo allowed the user to enter the tax amount manually regardless of the taxkey used.' => 'Der zweite Grund war, dass kivitendo zuließ, dass die Benutzer beliebige, von den tatsächlichen Steuerschlüsseln unabhängige Steuerbeträge eintrugen.',
   'The second way is to use Perl\'s CPAN module and let it download and install the module for you.' => 'Die zweite Variante besteht darin, Perls CPAN-Modul zu benutzen und es das Modul f&uuml;r Sie installieren zu lassen.',
   'The selected bank account does not exist anymore.' => 'Das ausgewählte Bankkonto existiert nicht mehr.',
index b49a851..a096dc6 100644 (file)
@@ -169,6 +169,11 @@ ACCESS=sales_order_edit
 module=controller.pl
 action=DeliveryPlan/list
 
+[AR--Reports--Financial Controlling]
+ACCESS=sales_order_edit
+module=controller.pl
+action=FinancialControllingReport/list
+
 [AP]
 
 [AP--Add RFQ]
@@ -414,6 +419,11 @@ module=rp.pl
 action=report
 report=projects
 
+[Reports--Financial Overview]
+ACCESS=report
+module=controller.pl
+action=FinancialOverview/list
+
 
 [Batch Printing]
 ACCESS=batch_printing
@@ -571,6 +581,10 @@ action=Business/list
 module=am.pl
 action=list_lead
 
+[System--Project Types]
+module=controller.pl
+action=ProjectType/list
+
 [System--Languages and translations]
 module=menu.pl
 action=acc_menu
diff --git a/sql/Pg-upgrade2/project_types.sql b/sql/Pg-upgrade2/project_types.sql
new file mode 100644 (file)
index 0000000..7d2c319
--- /dev/null
@@ -0,0 +1,25 @@
+-- @tag: project_types
+-- @description: Tabelle für Projekttypen
+-- @depends: release_3_0_0
+-- @charset: utf-8
+CREATE TABLE project_types (
+       id                       SERIAL,
+       position                 INTEGER NOT NULL,
+       description              TEXT,
+
+       PRIMARY KEY (id)
+);
+
+INSERT INTO project_types (position, description) VALUES (1, 'Standard');
+INSERT INTO project_types (position, description) VALUES (2, 'Festpreis');
+INSERT INTO project_types (position, description) VALUES (3, 'Support');
+
+ALTER TABLE project ADD COLUMN project_type_id INTEGER;
+ALTER TABLE project ADD FOREIGN KEY (project_type_id) REFERENCES project_types (id);
+
+UPDATE project SET project_type_id = (SELECT id FROM project_types WHERE description = 'Festpreis') WHERE type = 'Festpreis';
+UPDATE project SET project_type_id = (SELECT id FROM project_types WHERE description = 'Support')   WHERE type = 'Support';
+UPDATE project SET project_type_id = (SELECT id FROM project_types WHERE description = 'Standard')  WHERE project_type_id IS NULL;
+
+ALTER TABLE project ALTER COLUMN project_type_id SET NOT NULL;
+ALTER TABLE project DROP COLUMN type;
index 2588f90..856ae2e 100644 (file)
@@ -1,6 +1,6 @@
 use lib 't';
 
-use Test::More tests => 27;
+use Test::More tests => 28;
 use Test::Deep;
 use Data::Dumper;
 
@@ -281,3 +281,15 @@ test {
   query => [ 'customer.description' => 'test' ],
   with_objects => [ 'customer' ]
 }, 'with_objects: no duplicates', with_objects => [ 'customer' ];
+
+test {
+  part => {
+   'partnumber:substr::ilike' => '1',
+  },
+}, {
+  query => [
+   'part.partnumber', {
+     ilike => '%1%'
+   }
+ ]
+}, 'Regression check: prefixing of fallback filtering in relation with custom filters', class => 'SL::DB::Manager::OrderItem';
diff --git a/templates/webpages/financial_controlling_report/_filter.html b/templates/webpages/financial_controlling_report/_filter.html
new file mode 100644 (file)
index 0000000..fcae079
--- /dev/null
@@ -0,0 +1,54 @@
+[%- USE T8 %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE HTML %]
+<form action='controller.pl' method='post'>
+<div class='filter_toggle'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Show Filter' | $T8 %]</a>
+  [% SELF.filter_summary %]
+</div>
+<div class='filter_toggle' style='display:none'>
+<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
+ <table id='filter_table'>
+  <tr>
+   <th align="right">[% 'Customer' | $T8 %]</th>
+   <td>[% L.input_tag('filter.customer.name:substr::ilike', filter.customer.name_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Customer Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.customer.customernumber:substr::ilike', filter.customer.customernumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Order Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.ordnumber:substr::ilike', filter.ordnumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Project Number' | $T8 %]</th>
+   <td>[% L.input_tag('filter.globalproject.projectnumber:substr::ilike', filter.globalproject.projectnumber_substr__ilike, size = 20) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Project Type' | $T8 %]</th>
+   <td>[% L.select_tag('filter.globalproject.project_type_id', SELF.project_types, default=filter.globalproject.project_type_id, title_key='description', with_empty=1) %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Order Date' | $T8 %] [% 'From Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.transdate:date::ge', filter.transdate_date__ge, cal_align = 'BR') %]</td>
+  </tr>
+  <tr>
+   <th align="right">[% 'Order Date' | $T8 %] [% 'To Date' | $T8 %]</th>
+   <td>[% L.date_tag('filter.transdate:date::le', filter.transdate_date__le, cal_align = 'BR') %]</td>
+  </tr>
+ </table>
+
+[% L.hidden_tag('action', 'FinancialControllingReport/dispatch') %]
+[% L.hidden_tag('sort_by', FORM.sort_by) %]
+[% L.hidden_tag('sort_dir', FORM.sort_dir) %]
+[% L.hidden_tag('page', FORM.page) %]
+[% L.input_tag('action_list', LxERP.t8('Continue'), type = 'submit', class='submit')%]
+
+
+<a href='#' onClick='javascript:$("#filter_table input").val("");$("#filter_table option").prop("selected",0)'>[% 'Reset' | $T8 %]</a>
+
+</div>
+
+</form>
diff --git a/templates/webpages/financial_controlling_report/report_bottom.html b/templates/webpages/financial_controlling_report/report_bottom.html
new file mode 100644 (file)
index 0000000..da08e07
--- /dev/null
@@ -0,0 +1,2 @@
+[% USE L %]
+[%- L.paginate_controls(models=models) %]
diff --git a/templates/webpages/financial_controlling_report/report_top.html b/templates/webpages/financial_controlling_report/report_top.html
new file mode 100644 (file)
index 0000000..5fb399c
--- /dev/null
@@ -0,0 +1,3 @@
+[%- USE L %]
+[%- PROCESS 'financial_controlling_report/_filter.html' filter=SELF.filter %]
+ <hr>
diff --git a/templates/webpages/financial_overview/report_top.html b/templates/webpages/financial_overview/report_top.html
new file mode 100644 (file)
index 0000000..ae362ab
--- /dev/null
@@ -0,0 +1,18 @@
+[%- USE L %][% USE LxERP %]
+<form method="post" id="form" action="controller.pl">
+ [% L.hidden_tag('action', 'FinancialOverview/list') %]
+ <a href="[% SELF.url_for(action='list', year=(SELF.year - 1), subtotals_per_quarter=SELF.subtotals_per_quarter) %]">&laquo; [%- LxERP.t8("Prior year") %]: [% SELF.year - 1 %]</a>
+ |
+
+ [% LxERP.t8("Current year") %]:
+ [% L.select_tag('year', YEARS_TO_LIST, default=SELF.year, onchange='$("#form").submit();') %]
+
+ [% IF SELF.year < SELF.current_year %]
+  |
+  <a href="[% SELF.url_for(action='list', year=(SELF.year + 1), subtotals_per_quarter=SELF.subtotals_per_quarter) %]">[%- LxERP.t8("Following year") %]: [% SELF.year + 1 %] &raquo;</a>
+ [% END %]
+
+ <br>
+ [% L.checkbox_tag('subtotals_per_quarter', checked=SELF.subtotals_per_quarter, label=LxERP.t8('Subtotals per quarter'), onchange='$("#form").submit();') %]
+</form>
+<hr>
index d243afd..9ec39e0 100644 (file)
@@ -42,8 +42,8 @@
      </tr>
 
      <tr>
-      <th align="right">[% 'Type' | $T8 %]</th>
-      <td>[% L.input_tag('project.type', SELF.project.type, size=60) %]</td>
+      <th align="right">[% 'Project Type' | $T8 %]</th>
+      <td>[% L.select_tag('project.project_type_id', ALL_PROJECT_TYPES, default=SELF.project.project_type_id, title_key='description', style='width: 300px') %]</td>
      </tr>
 
      <tr>
index 44bf0d8..1f3e33b 100644 (file)
@@ -27,8 +27,8 @@
     </tr>
 
     <tr>
-     <th align="right">[% 'Type' | $T8 %]</th>
-     <td>[% L.input_tag('filter.type:substr::ilike', filter.type_substr__ilike, size=20) %]</td>
+     <th align="right">[% 'Project Type' | $T8 %]</th>
+     <td>[% L.select_tag('filter.project_type_id', ALL_PROJECT_TYPES, default=filter.project_type_id, title_key='description', with_empty=1) %]</td>
     </tr>
 
     [% CUSTOM_VARIABLES_FILTER_CODE %]
diff --git a/templates/webpages/project_type/form.html b/templates/webpages/project_type/form.html
new file mode 100755 (executable)
index 0000000..692cb4a
--- /dev/null
@@ -0,0 +1,25 @@
+[% USE HTML %][% USE L %][% USE LxERP %]
+
+ <form method="post" action="controller.pl">
+  <div class="listtop">[% FORM.title %]</div>
+
+[%- INCLUDE 'common/flash.html' %]
+
+  <table>
+   <tr>
+    <td>[% LxERP.t8('Description') %]</td>
+    <td>[% L.input_tag("project_type.description" SELF.project_type.description) %]</td>
+   </tr>
+  </table>
+
+  <p>
+   [% L.hidden_tag("id", SELF.project_type.id) %]
+   [% L.hidden_tag("action", "ProjectType/dispatch") %]
+   [% L.submit_tag("action_" _ (SELF.project_type.id ? 'update' : 'create'), LxERP.t8('Save')) %]
+   [%- IF SELF.project_type.id %]
+    [% L.submit_tag("action_destroy", LxERP.t8('Delete'), confirm=LxERP.t8('Do you really want to delete this object?')) %]
+   [%- END %]
+   <a href="[% SELF.url_for(action => 'list') %]">[% LxERP.t8('Abort') %]</a>
+  </p>
+
+ </form>
diff --git a/templates/webpages/project_type/list.html b/templates/webpages/project_type/list.html
new file mode 100644 (file)
index 0000000..fc45c0d
--- /dev/null
@@ -0,0 +1,42 @@
+[% USE HTML %][% USE T8 %][% USE L %][% USE LxERP %]
+
+ <div class="listtop">[% FORM.title %]</div>
+
+[%- INCLUDE 'common/flash.html' %]
+
+ <form method="post" action="controller.pl">
+  [% IF !PROJECT_TYPES.size %]
+   <p>
+    [%- 'No project type has been created yet.' | $T8 %]
+   </p>
+
+  [%- ELSE %]
+   <table id="project_type_list">
+    <thead>
+    <tr class="listheading">
+     <th align="center"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></th>
+     <th>[%- 'Description' | $T8 %]</th>
+    </tr>
+    </thead>
+
+    <tbody>
+    [%- FOREACH project_type = PROJECT_TYPES %]
+    <tr class="listrow[% loop.count % 2 %]" id="project_type_id_[% project_type.id %]">
+     <td align="center" class="dragdrop"><img src="image/updown.png" alt="[ LxERP.t8('reorder item') %]"></td>
+     <td>
+      <a href="[% SELF.url_for(action => 'edit', id => project_type.id) %]">
+       [%- HTML.escape(project_type.description) %]
+      </a>
+     </td>
+    </tr>
+    [%- END %]
+    </tbody>
+   </table>
+  [%- END %]
+
+  <p>
+   <a href="[% SELF.url_for(action => 'new') %]">[%- 'Create new project type' | $T8 %]</a>
+  </p>
+ </form>
+
+ [% L.sortable_element('#project_type_list tbody', url => 'controller.pl?action=ProjectType/reorder', with => 'project_type_id') %]