commit 5d7a7cb5da29bf5ba69e1bff22a2a2620d989ee8
Author: Bernd Bleßmann <bernd@kivitendo-premium.de>
Date:   Fri Jul 14 15:59:50 2023 +0200

    "Beleg ohne Positionen speichern erlauben" in Mandantenkonfig einstellbar

diff --git a/SL/Controller/ClientConfig.pm b/SL/Controller/ClientConfig.pm
index c44aac378..8c4497dd2 100644
--- a/SL/Controller/ClientConfig.pm
+++ b/SL/Controller/ClientConfig.pm
@@ -17,6 +17,9 @@ use SL::Helper::Flash;
 use SL::Locale::String qw(t8);
 use SL::PriceSource::ALL;
 use SL::Template;
+use SL::Controller::DeliveryOrder;
+use SL::Controller::Order;
+use SL::Controller::Reclamation;
 use SL::Controller::TopQuickSearch;
 use SL::DB::Helper::AccountingPeriod qw(get_balance_startdate_method_options);
 use SL::VATIDNr;
@@ -28,7 +31,7 @@ use Rose::Object::MakeMethods::Generic (
   'scalar --get_set_init' => [ qw(defaults all_warehouses all_weightunits all_languages all_currencies all_templates all_price_sources h_unit_name available_quick_search_modules
                                   all_project_statuses all_project_types zugferd_settings
                                   posting_options payment_options accounting_options inventory_options profit_options balance_startdate_method_options
-                                  displayable_name_specs_by_module) ],
+                                  displayable_name_specs_by_module available_documents_with_no_positions) ],
 );
 
 sub action_edit {
@@ -232,6 +235,27 @@ sub init_displayable_name_specs_by_module {
   };
 }
 
+sub init_available_documents_with_no_positions {
+  return [] if !$::instance_conf->get_feature_experimental_order;
+
+  my @docs = ( @{SL::Controller::Order        ->new->valid_types},
+               @{SL::Controller::DeliveryOrder->new->valid_types},
+               @{SL::Controller::Reclamation  ->new->valid_types} );
+
+  # normal delivery orders are not yet handeld by new controller code
+  @docs = grep { $_ ne 'sales_delivery_order' && $_ ne 'purchase_delivery_order' } @docs;
+
+  # $::form->get_formname_translation does not distinguish between sales and purchase
+  @docs = grep { $_ ne 'sales_reclamation' && $_ ne 'purchase_reclamation' } @docs;
+
+  my @available_docs = map { {name => $_, description => $::form->get_formname_translation($_)} } @docs;
+
+  push @available_docs, {name => 'sales_reclamation',    description => t8('Sales Reclamation')};
+  push @available_docs, {name => 'purchase_reclamation', description => t8('Purchase Reclamation')};
+
+  return \@available_docs;
+}
+
 #
 # filters
 #
diff --git a/SL/Controller/DeliveryOrder.pm b/SL/Controller/DeliveryOrder.pm
index ec73c8707..0978202c2 100644
--- a/SL/Controller/DeliveryOrder.pm
+++ b/SL/Controller/DeliveryOrder.pm
@@ -1661,7 +1661,7 @@ sub save {
   my $errors = [];
   my $db     = $self->order->db;
 
-  if (scalar @{$self->order->items} == 0) {
+  if (scalar @{$self->order->items} == 0 && !grep { $self->type eq $_ } @{$::instance_conf->get_allowed_documents_with_no_positions() || []}) {
     return [t8('The action you\'ve chosen has not been executed because the document does not contain any item yet.')];
   }
 
diff --git a/SL/Controller/Order.pm b/SL/Controller/Order.pm
index 13cb2326c..4adecb8ce 100644
--- a/SL/Controller/Order.pm
+++ b/SL/Controller/Order.pm
@@ -2188,7 +2188,7 @@ sub save {
   my $errors = [];
   my $db     = $self->order->db;
 
-  if (scalar @{$self->order->items} == 0) {
+  if (scalar @{$self->order->items} == 0 && !grep { $self->type eq $_ } @{$::instance_conf->get_allowed_documents_with_no_positions() || []}) {
     return [t8('The action you\'ve chosen has not been executed because the document does not contain any item yet.')];
   }
 
diff --git a/SL/Controller/Reclamation.pm b/SL/Controller/Reclamation.pm
index 5904d87d3..168b64d4c 100644
--- a/SL/Controller/Reclamation.pm
+++ b/SL/Controller/Reclamation.pm
@@ -1750,7 +1750,7 @@ sub save {
   my $errors = [];
   my $db     = $self->reclamation->db;
 
-  if (scalar @{$self->reclamation->items} == 0) {
+  if (scalar @{$self->reclamation->items} == 0 && !grep { $self->type eq $_ } @{$::instance_conf->get_allowed_documents_with_no_positions() || []}) {
     return [t8('The action you\'ve chosen has not been executed because the document does not contain any item yet.')];
   }
 
diff --git a/SL/DB/MetaSetup/Default.pm b/SL/DB/MetaSetup/Default.pm
index 3e56aa7c1..7b5dd0571 100644
--- a/SL/DB/MetaSetup/Default.pm
+++ b/SL/DB/MetaSetup/Default.pm
@@ -22,6 +22,7 @@ __PACKAGE__->meta->columns(
   allow_new_purchase_invoice                => { type => 'boolean', default => 'true', not_null => 1 },
   allow_sales_invoice_from_sales_order      => { type => 'boolean', default => 'true', not_null => 1 },
   allow_sales_invoice_from_sales_quotation  => { type => 'boolean', default => 'true', not_null => 1 },
+  allowed_documents_with_no_positions       => { type => 'array' },
   always_record_links_from_order            => { type => 'boolean', default => 'false' },
   ap_add_doc                                => { type => 'boolean', default => 'false', not_null => 1 },
   ap_changeable                             => { type => 'integer', default => 2, not_null => 1 },
diff --git a/doc/changelog b/doc/changelog
index ad7e7e9cc..144b973cb 100644
--- a/doc/changelog
+++ b/doc/changelog
@@ -39,6 +39,9 @@ Kleinere neue Features und Detailverbesserungen:
   - Bei der Bearbeitung von VK-Angebote und VK-Aufträgen können die Emails aus
     den angelegten Ordner im Email-Server automatisch importiert werden (s.a.
     kivitendo.conf)
+  - Belege ("neue" Controller, sprich Angebot/Auftrag/Beistelllieferschein/RMA/
+    Reklamation) können nicht mehr ohne Positionen gespeichert werden. Ausnahmen
+    lassen sich in der Mandantenkonfiguration einstellen.
 
 2023-04-05 - Release 3.8.0
 
diff --git a/locale/de/all b/locale/de/all
index 96ca0e2a5..9b21f1ec6 100644
--- a/locale/de/all
+++ b/locale/de/all
@@ -1275,6 +1275,7 @@ $self->{texts} = {
   'Documentation about variables in a new window/tab' => 'Dokumentation zu Variablen in neuem Fenster/Tab',
   'Documents'                   => 'Dokumente',
   'Documents in the WebDAV repository' => 'Dokumente im WebDAV-Repository',
+  'Documents which are allowed to be saved with no positions' => 'Belege, die ohne Positionen gespeichert werden dürfen',
   'Don\'t include a printout of the record with the email' => 'Keinen Belegausdruck mit E-Mail schicken',
   'Don\'t include a printout of the record with the email, only selected files' => 'Keinen Belegausdruck mit E-Mail schicken, sondern nur ausgewählte Dateien',
   'Done'                        => 'Fertig',
diff --git a/locale/en/all b/locale/en/all
index e955e927e..5a5f7ba49 100644
--- a/locale/en/all
+++ b/locale/en/all
@@ -1275,6 +1275,7 @@ $self->{texts} = {
   'Documentation about variables in a new window/tab' => '',
   'Documents'                   => '',
   'Documents in the WebDAV repository' => '',
+  'Documents which are allowed to be saved with no positions' => '',
   'Don\'t include a printout of the record with the email' => '',
   'Don\'t include a printout of the record with the email, only selected files' => '',
   'Done'                        => '',
diff --git a/sql/Pg-upgrade2/defaults_allowed_documents_with_no_positions.sql b/sql/Pg-upgrade2/defaults_allowed_documents_with_no_positions.sql
new file mode 100644
index 000000000..20ffb2449
--- /dev/null
+++ b/sql/Pg-upgrade2/defaults_allowed_documents_with_no_positions.sql
@@ -0,0 +1,5 @@
+-- @tag: defaults_allowed_documents_with_no_positions
+-- @description: Mandantenkonfiguration für Belege, die ohne Positionen gespeichert werden können
+-- @depends: release_3_8_0
+
+ALTER TABLE defaults ADD COLUMN allowed_documents_with_no_positions TEXT[];
diff --git a/templates/design40_webpages/client_config/_features.html b/templates/design40_webpages/client_config/_features.html
index ffcfbb8cd..392b2dec3 100644
--- a/templates/design40_webpages/client_config/_features.html
+++ b/templates/design40_webpages/client_config/_features.html
@@ -403,6 +403,22 @@
         <td>[% L.yes_no_tag("defaults.lock_oe_subversions", SELF.defaults.lock_oe_subversions) %]</td>
         <td class="long-desc">[% LxERP.t8("If enabled RFQs, Quotations and Orders can only be send once via email. By creating another subversion the current record gets a minor subversion and can be send again via email.") %]</td>
       </tr>
+      <tr>
+        <th>[% 'Documents which are allowed to be saved with no positions' | $T8 %]</th>
+        <td colspan="2">
+          <div>
+            [% L.select_tag("defaults.allowed_documents_with_no_positions[]",
+                            SELF.available_documents_with_no_positions,
+                            default=SELF.defaults.allowed_documents_with_no_positions,
+                            multiple=1,
+                            with_empty=1,
+                            value_key="name",
+                            title_key="description",
+                            id="defaults_allowed_documents_with_no_positions",
+                            size=SELF.available_documents_with_no_positions.size+1) %]
+          </div>
+        </td>
+      </tr>
       <tr>
         <th class="caption" colspan="3">[% LxERP.t8('Transport and service costs reminder') %]</th>
       </tr>
diff --git a/templates/webpages/client_config/_features.html b/templates/webpages/client_config/_features.html
index 049666a67..533ee4cd7 100644
--- a/templates/webpages/client_config/_features.html
+++ b/templates/webpages/client_config/_features.html
@@ -311,6 +311,23 @@
    <td>[% L.yes_no_tag("defaults.lock_oe_subversions", SELF.defaults.lock_oe_subversions) %]</td>
    <td>[% LxERP.t8("If enabled RFQs, Quotations and Orders can only be send once via email. By creating another subversion the current record gets a minor subversion and can be send again via email.") %]</td>
   </tr>
+  <tr>
+    <td align="right">[% 'Documents which are allowed to be saved with no positions' | $T8 %]</td>
+    <td colspan=2>
+      <div class="clearfix">
+        [% L.select_tag("defaults.allowed_documents_with_no_positions[]",
+                        SELF.available_documents_with_no_positions,
+                        default=SELF.defaults.allowed_documents_with_no_positions,
+                        multiple=1,
+                        with_empty=1,
+                        value_key="name",
+                        title_key="description",
+                        id="defaults_allowed_documents_with_no_positions",
+                        size=SELF.available_documents_with_no_positions.size+1) %]
+      </div>
+    </td>
+  </tr>
+
   <tr><td class="listheading" colspan="4">[% LxERP.t8("E-mail") %]</td></tr>
 
   <tr>
