commit 5d27f2bd27c224689a829bac4fe8cd91b702a15b
Author: Moritz Bunkus <m.bunkus@linet.de>
Date:   Thu Feb 9 14:43:57 2023 +0100

    Erzeugnisstammdaten: Exportmöglichkeit für Bestandteilliste als CSV

diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm
index 944b02292..46ec41727 100644
--- a/SL/Controller/Part.pm
+++ b/SL/Controller/Part.pm
@@ -11,18 +11,22 @@ use SL::DB::Shop;
 use SL::Controller::Helper::GetModels;
 use SL::Locale::String qw(t8);
 use SL::JSON;
+use File::Temp;
 use List::Util qw(sum);
 use List::UtilsBy qw(extract_by);
 use SL::Helper::Flash;
 use Data::Dumper;
 use DateTime;
+use POSIX qw(strftime);
 use SL::DB::History;
 use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
 use SL::CVar;
 use SL::MoreCommon qw(save_form);
 use Carp;
 use SL::Presenter::EscapedText qw(escape is_escaped);
+use SL::Presenter::Part;
 use SL::Presenter::Tag qw(select_tag);
+use Text::CSV_XS;
 
 use Rose::Object::MakeMethods::Generic (
   'scalar --get_set_init' => [ qw(parts models part p warehouses multi_items_models
@@ -688,6 +692,70 @@ sub action_show {
   }
 }
 
+sub action_export_assembly_components {
+  my ($self) = @_;
+
+  my @rows = ([
+    $::locale->text('Partnumber'),
+    $::locale->text('Description'),
+    $::locale->text('Type'),
+    $::locale->text('Classification'),
+    $::locale->text('Qty'),
+    $::locale->text('Unit'),
+    $::locale->text('BOM'),
+    $::locale->text('Line Total'),
+    $::locale->text('Sellprice'),
+    $::locale->text('Lastcost'),
+    $::locale->text('Partsgroup'),
+  ]);
+
+  foreach my $item (@{ $self->part->items }) {
+    my $part = $item->part;
+
+    my @row = (
+      $part->partnumber,
+      $part->description,
+      SL::Presenter::Part::type_abbreviation($part->part_type),
+      SL::Presenter::Part::classification_abbreviation($part->classification_id),
+      $item->qty_as_number,
+      $part->unit,
+      $item->bom ? $::locale->text('yes') : $::locale->text('no'),
+      $::form->format_amount(\%::myconfig, $item->linetotal_sellprice, 3, 0),
+      $part->sellprice_as_number,
+      $part->lastcost_as_number,
+      $part->partsgroup ? $part->partsgroup->partsgroup : '',
+    );
+
+    push @rows, \@row;
+  }
+
+  my $csv = Text::CSV_XS->new({
+    sep_char => ';',
+    eol      => "\n",
+    binary   => 1,
+  });
+
+  my ($file_handle, $file_name) = File::Temp::tempfile;
+
+  binmode $file_handle, ":encoding(utf8)";
+
+  $csv->print($file_handle, $_) for @rows;
+
+  $file_handle->close;
+
+  my $timestamp       = strftime('_%Y-%m-%d_%H-%M-%S', localtime());
+  my $part_number     = $self->part->partnumber;
+  $part_number        =~ s{[^[:word:]]+}{_}g;
+  my $attachment_name = sprintf('assembly_components_%s_%s.csv', $part_number, $timestamp);
+
+  $self->send_file(
+    $file_name,
+    content_type => 'text/csv',
+    name         => $attachment_name,
+  );
+
+}
+
 # helper functions
 sub validate_add_items {
   scalar @{$::form->{add_items}};
@@ -1394,6 +1462,23 @@ sub _setup_form_action_bar {
         ],
       ],
 
+      combobox => [
+        action => [
+          t8('Export'),
+          only_if => $self->part->is_assembly,
+        ],
+        action => [
+          t8('Assembly items'),
+          submit   => [ '#ic', { action => "Part/export_assembly_components" } ],
+          checks   => ['kivi.validate_form'],
+          disabled => !$self->part->id                                    ? t8('The object has not been saved yet.')
+                    : !$may_edit                                          ? t8('You do not have the permissions to access this function.')
+                    : !$::auth->assert('purchase_order_edit', 'may fail') ? t8('You do not have the permissions to access this function.')
+                    :                                                       undef,
+          only_if  => $self->part->is_assembly,
+        ],
+      ],
+
       action => [
         t8('Abort'),
         submit   => [ '#ic', { action => "Part/abort" } ],
diff --git a/locale/de/all b/locale/de/all
index 677182782..6ccd2ab7c 100644
--- a/locale/de/all
+++ b/locale/de/all
@@ -709,6 +709,7 @@ $self->{texts} = {
   'Choose a Tax Number'         => 'Bitte eine Steuernummer angeben',
   'Choose bank account for reconciliation' => 'Wählen Sie das Bankkonto für den Kontenabgleich',
   'City'                        => 'Stadt',
+  'Classification'              => 'Klassifizierung',
   'Clear'                       => 'Löschen',
   'Clear fields'                => 'Felder leeren',
   'Cleared Balance'             => 'abgeschlossen',
