Merge pull request #35 from kivitendo/f-shopware6-rebase1
authorJan Büren <jan@kivitendo-premium.de>
Fri, 11 Feb 2022 14:23:40 +0000 (15:23 +0100)
committerGitHub <noreply@github.com>
Fri, 11 Feb 2022 14:23:40 +0000 (15:23 +0100)
F shopware6 rebase1

24 files changed:
SL/Controller/ShopOrder.pm
SL/Controller/ShopPart.pm
SL/DB/MetaSetup/Shop.pm
SL/DB/MetaSetup/ShopOrder.pm
SL/DB/MetaSetup/ShopOrderItem.pm
SL/DB/Shop.pm
SL/DB/ShopOrder.pm
SL/DB/ShopPart.pm
SL/InstallationCheck.pm
SL/ShopConnector/ALL.pm
SL/ShopConnector/Base.pm
SL/ShopConnector/Shopware.pm
SL/ShopConnector/Shopware6.pm [new file with mode: 0644]
js/kivi.ShopPart.js
locale/de/all
locale/en/all
sql/Pg-upgrade2/shop_orders_update_4.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_5.sql [new file with mode: 0644]
sql/Pg-upgrade2/shops_6.sql [new file with mode: 0644]
templates/webpages/part/_shop.html
templates/webpages/shop_part/categories.html
templates/webpages/shop_part/edit.html
templates/webpages/shops/form.html
templates/webpages/shops/test_shop_connection.html

index 2da4a41..3e61c63 100644 (file)
@@ -63,7 +63,7 @@ sub action_get_orders {
     if($shop_fetched->{error}){
       flash_later('error', t8('From shop "#1" :  #2 ', $shop_fetched->{shop_description}, $shop_fetched->{message},));
     }else{
-      flash_later('info', t8('From shop #1 :  #2 shoporders have been fetched.', $shop_fetched->{description}, $shop_fetched->{number_of_orders},));
+      flash_later('info', t8('From shop #1 :  #2 shoporders have been fetched.', $shop_fetched->{shop_description}, $shop_fetched->{number_of_orders},));
     }
   }
 
index fb2fe92..6ab6507 100644 (file)
@@ -266,7 +266,6 @@ sub action_mass_upload {
 
 sub action_update {
   my ($self) = @_;
-
   $self->create_or_update;
 }
 
@@ -288,17 +287,15 @@ sub create_or_update {
   my ($self) = @_;
 
   my $is_new = !$self->shop_part->id;
-
   my $params = delete($::form->{shop_part}) || { };
-
   $self->shop_part->assign_attributes(%{ $params });
-
   $self->shop_part->save;
 
   my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
-if(!$is_new){
-  flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
-  $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
+
+  if(!$is_new){
+    flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
+    $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
            ->html('#shop_part_active_' . $self->shop_part->id, $self->shop_part->active)
            ->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
            ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
index 1110fd0..adb6b3a 100644 (file)
@@ -9,27 +9,29 @@ use parent qw(SL::DB::Object);
 __PACKAGE__->meta->table('shops');
 
 __PACKAGE__->meta->columns(
-  connector               => { type => 'text' },
-  description             => { type => 'text' },
-  id                      => { type => 'serial', not_null => 1 },
-  itime                   => { type => 'timestamp', default => 'now()' },
-  last_order_number       => { type => 'integer' },
-  login                   => { type => 'text' },
-  mtime                   => { type => 'timestamp', default => 'now()' },
-  obsolete                => { type => 'boolean', default => 'false', not_null => 1 },
-  orders_to_fetch         => { type => 'integer' },
-  password                => { type => 'text' },
-  path                    => { type => 'text', default => '/', not_null => 1 },
-  port                    => { type => 'integer' },
-  price_source            => { type => 'text' },
-  pricetype               => { type => 'text' },
-  protocol                => { type => 'text', default => 'http', not_null => 1 },
-  realm                   => { type => 'text' },
-  server                  => { type => 'text' },
-  shipping_costs_parts_id => { type => 'integer' },
-  sortkey                 => { type => 'integer' },
-  taxzone_id              => { type => 'integer' },
-  transaction_description => { type => 'text' },
+  connector                => { type => 'text' },
+  description              => { type => 'text' },
+  id                       => { type => 'serial', not_null => 1 },
+  itime                    => { type => 'timestamp', default => 'now()' },
+  last_order_number        => { type => 'integer' },
+  login                    => { type => 'text' },
+  mtime                    => { type => 'timestamp', default => 'now()' },
+  obsolete                 => { type => 'boolean', default => 'false', not_null => 1 },
+  orders_to_fetch          => { type => 'integer' },
+  password                 => { type => 'text' },
+  path                     => { type => 'text', default => '/', not_null => 1 },
+  port                     => { type => 'integer' },
+  price_source             => { type => 'text' },
+  pricetype                => { type => 'text' },
+  protocol                 => { type => 'text', default => 'http', not_null => 1 },
+  proxy                    => { type => 'text', default => '' },
+  realm                    => { type => 'text' },
+  server                   => { type => 'text' },
+  shipping_costs_parts_id  => { type => 'integer' },
+  sortkey                  => { type => 'integer' },
+  taxzone_id               => { type => 'integer' },
+  transaction_description  => { type => 'text' },
+  use_part_longdescription => { type => 'boolean', default => 'false' },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
index cd31c18..6b685fa 100644 (file)
@@ -77,7 +77,7 @@ __PACKAGE__->meta->columns(
   shop_customer_number   => { type => 'text' },
   shop_id                => { type => 'integer' },
   shop_ordernumber       => { type => 'text' },
-  shop_trans_id          => { type => 'integer', not_null => 1 },
+  shop_trans_id          => { type => 'text', not_null => 1 },
   tax_included           => { type => 'boolean' },
   transfer_date          => { type => 'date' },
   transferred            => { type => 'boolean', default => 'false' },
index dfb2255..ae7d21a 100644 (file)
@@ -17,7 +17,7 @@ __PACKAGE__->meta->columns(
   price               => { type => 'numeric', precision => 15, scale => 5 },
   quantity            => { type => 'numeric', precision => 25, scale => 5 },
   shop_order_id       => { type => 'integer' },
-  shop_trans_id       => { type => 'integer', not_null => 1 },
+  shop_trans_id       => { type => 'text', not_null => 1 },
   tax_rate            => { type => 'numeric', precision => 15, scale => 2 },
 );
 
index 63f988f..c5e652a 100644 (file)
@@ -16,10 +16,26 @@ sub validate {
   my ($self) = @_;
 
   my @errors;
-
+  # critical checks
   push @errors, $::locale->text('The description is missing.') unless $self->{description};
-  push @errors, $::locale->text('The path is missing.') unless $self->{path};
-
+  push @errors, $::locale->text('The path is missing.')        unless $self->{path};
+  push @errors, $::locale->text('The Host Name is missing')    unless $self->{server};
+  push @errors, $::locale->text('The Host Name seems invalid') unless $self->{server} =~ m/[0-9A-Za-z].\.[0-9A-Za-z]/;
+  push @errors, $::locale->text('The Protocol for Host Name seems invalid (expected: http:// or https://)!')
+                                                               if ($self->{server} =~ m/:/ && $self->{server} !~ m/(^https:\/\/|^http:\/\/)/);
+  push @errors, $::locale->text('The Proxy Name seems invalid') . $self->{proxy} . ':' unless !$self->{proxy} ||  $self->{proxy} =~ m/[0-9A-Za-z].\.[0-9A-Za-z]/;
+  push @errors, $::locale->text('Orders to fetch neeeds a positive Integer')
+                                                               unless $self->{orders_to_fetch} > 0;
+
+  # not yet implemented checks
+  push @errors, $::locale->text('Transaction Description is not yet implemented')    if $self->{transaction_description};
+  if ($self->{connector} eq 'shopware6') {
+    push @errors, $::locale->text('Shipping cost article is not implemented')        if $self->{shipping_costs_parts_id};
+    push @errors, $::locale->text('Fetch from last order number is not implemented') if $self->{last_order_number};
+  } else {
+    push @errors, $::locale->text('Use Long Description from Parts is only for Shopware6 implemented')
+      if $self->{use_part_longdescription};
+  }
   return @errors;
 }
 
index ae5d856..0039902 100644 (file)
@@ -48,7 +48,8 @@ sub convert_to_sales_order {
     }else{
       my $current_order_item = SL::DB::OrderItem->new(
         parts_id            => $part->id,
-        description         => $part->description,
+        description         => $_->description, # description from the shop
+        longdescription     => $part->notes,    # longdescription from parts. TODO locales
         qty                 => $_->quantity,
         sellprice           => $_->price,
         unit                => $part->unit,
index 1df77fd..7c67bb1 100644 (file)
@@ -50,16 +50,20 @@ sub get_tax_and_price {
 }
 
 sub get_images {
-  my ( $self ) = @_;
+  my ($self, %params) = @_;
 
   require SL::DB::ShopImage;
   my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $self->{part_id}, ], with_objects => 'file', sort_by => 'position' );
   my @upload_img = ();
   foreach my $img (@{ $images }) {
     my $file               = SL::File->get(id => $img->file->id );
-    my ($path, $extension) = (split /\./, $file->file_name);
+    # no good: split("\." , 202.220.pdf) -> invaild extension 220
+    # file->extension should be in SL::File, a valid extension may also be 'tar.gz'
+    my ($path, $extension) = split(/\.([^\.]+)$/, $file->file_name);
     my $content            = File::Slurp::read_file($file->get_file);
-    my $temp ={ ( link        => 'data:' . $file->mime_type . ';base64,' . MIME::Base64::encode($content, ""), #$content, # MIME::Base64::encode($content),
+
+    my $temp ={ (
+                  link        => $params{want_binary} ? $content : 'data:' . $file->mime_type . ';base64,' . MIME::Base64::encode($content, ""),
                   description => $img->file->title,
                   position    => $img->position,
                   extension   => $extension,
index 7712d09..deaa8c1 100644 (file)
@@ -55,6 +55,7 @@ BEGIN {
   { name => "PBKDF2::Tiny",    version => '0.005', url => "http://search.cpan.org/~dagolden/",  debian => 'libpbkdf2-tiny-perl' },
   { name => "PDF::API2",       version => '2.000', url => "http://search.cpan.org/~areibens/",  debian => 'libpdf-api2-perl' },
   { name => "Regexp::IPv6",    version => '0.03',  url => "http://search.cpan.org/~salva/",     debian => 'libregexp-ipv6-perl' },
+  { name => "REST::Client",                        url => "https://metacpan.org/pod/REST::Client", debian => 'librest-client-perl' },
   { name => "Rose::Object",                        url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-object-perl' },
   { name => "Rose::DB",                            url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-perl' },
   { name => "Rose::DB::Object", version => 0.788,  url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-object-perl' },
index 3f29c99..1b1b0c8 100644 (file)
@@ -3,26 +3,25 @@ package SL::ShopConnector::ALL;
 use strict;
 
 use SL::ShopConnector::Shopware;
+use SL::ShopConnector::Shopware6;
 use SL::ShopConnector::WooCommerce;
 
 my %shop_connector_by_name = (
   shopware    => 'SL::ShopConnector::Shopware',
-  woocommerce    => 'SL::ShopConnector::WooCommerce',
-);
-
-my %shop_connector_by_connector = (
-  shopware   => 'SL::ShopConnector::Shopware',
+  shopware6   => 'SL::ShopConnector::Shopware6',
   woocommerce => 'SL::ShopConnector::WooCommerce',
 );
 
 my @shop_connector_order = qw(
   woocommerce
   shopware
+  shopware6
 );
 
 my @shop_connectors = (
-  { id => "shopware",   description => "Shopware" },
-  { id => "woocommerce",   description => "WooCommerce" },
+  { id => "shopware",    description => "Shopware" },
+  { id => "shopware6",   description => "Shopware6" },
+  { id => "woocommerce", description => "WooCommerce" },
 );
 
 
@@ -34,10 +33,6 @@ sub shop_connector_class_by_name {
   $shop_connector_by_name{$_[1]};
 }
 
-sub shop_connector_class_by_connector {
-  $shop_connector_by_connector{$_[1]};
-}
-
 sub connectors {
   \@shop_connectors;
 }
index 56127e4..1dc4a2e 100644 (file)
@@ -7,17 +7,63 @@ use Rose::Object::MakeMethods::Generic (
   scalar => [ qw(config) ],
 );
 
-sub get_one_order  { die 'get_one_order needs to be implemented' }
+sub get_one_order  {
+  die 'get_one_order needs to be implemented';
+
+  my ($self, $ordnumber) = @_;
+  my %fetched_order;
+
+  # 1. fetch the order and import it as a kivi order
+  # 2. update the order state for report
+  # 3. return a hash with either success or error state
+  my $one_order; # REST call
+
+  my $error = $self->import_data_to_shop_order($one_order);
+
+  $self->set_orderstatus($one_order->{id}, "fetched") unless $error;
+
+  return \(
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      number_of_orders => $error ? 0 : 1,
+      message          => $error ? "Error: $error->{msg}"  : '',
+      error            => $error ? 1 : 0,
+    );
+}
+
 
 sub get_new_orders { die 'get_order needs to be implemented' }
 
-sub update_part    { die 'update_part needs to be implemented' }
+sub update_part    {
+  die 'update_part needs to be implemented';
+
+  my ($self, $shop_part, $todo) = @_;
+  #shop_part is passed as a param
+  die "Need a valid Shop Part for updating Part" unless ref($shop_part) eq 'SL::DB::ShopPart';
+  die "Invalid todo for updating Part"           unless $todo =~ m/(price|stock|price_stock|active|all)/;
+
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+  die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
+
+  my $success;
+
+  return $success ? 1 : 0;
+}
 
 sub get_article    { die 'get_article needs to be implemented' }
 
 sub get_categories { die 'get_categories needs to be implemented' }
 
-sub get_version    { die 'get_version needs to be implemented' }
+sub get_version    {
+
+  die 'get_version needs to be implemented';
+  # has to return a hashref with this structure:
+  # version has to return the connection error message
+  my $connect = {};
+  $connect->{success}         = 0 || 1;
+  $connect->{data}->{version} = '1234';
+  return $connect;
+}
 
 sub set_orderstatus { die 'set_orderstatus needs to be implemented' }
 
@@ -40,11 +86,37 @@ __END__
 
 =over 4
 
-=item C<get_one_order>
+=item C<get_one_order $ordnumber>
+
+Needs a order number and fetch (one or more) orders
+which are returned by the Shop system. The function
+has to take care of getting the order including customer
+and item information to kivi.
+It has to return a hash with either the number of succesful
+imported order or within the same hash structure a error message.
+
+
 
 =item C<get_new_orders>
 
-=item C<update_part>
+=item C<update_part $shop_part $todo>
+
+Updates one Part including all metadata and Images in a Shop.
+Needs a valid 'SL::DB::ShopPart' as first parameter and a requested action
+as second parameter. Valid values for the second parameter are
+"price, stock, price_stock, active, all".
+The method updates either all metadata or a subset.
+Name and action for the subsets:
+
+ price       => updates only the price
+ stock       => updates only the available stock (qty)
+ price_stock => combines both predecessors
+ active      => updates only the state of the shop part
+
+Images should always be updated, regardless of the requested action.
+Returns 1 if all updates were executed successful.
+
+
 
 =item C<get_article>
 
@@ -52,8 +124,23 @@ __END__
 
 =item C<get_version>
 
+IMPORTANT: This call is used to test the connection and if succesful
+it returns the version number of the shop. If not succesful the
+returning function has to make sure a error string is returned in
+the same data structure. Details of the returning hashref:
+
+ my $connect = {};
+ $connect->{success}         = 0 || 1;
+ $connect->{data}->{version} = '1234';
+ return $connect;
+
 =item C<set_orderstatus>
 
+Sets the state of the order in the Shop.
+Valid values depend on the Shop API, common states
+are delivered, fetched, paid, in progress ...
+
+
 =back
 
 =head1 SEE ALSO
index a777ddf..65c42fd 100644 (file)
@@ -468,7 +468,7 @@ __END__
 
 =head1 NAME
 
-SL::Shopconnecter::Shopware - connector for shopware 5
+SL::Shopconnector::Shopware - connector for shopware 5
 
 =head1 SYNOPSIS
 
diff --git a/SL/ShopConnector/Shopware6.pm b/SL/ShopConnector/Shopware6.pm
new file mode 100644 (file)
index 0000000..27c569a
--- /dev/null
@@ -0,0 +1,1092 @@
+package SL::ShopConnector::Shopware6;
+
+use strict;
+
+use parent qw(SL::ShopConnector::Base);
+
+use Carp;
+use Encode qw(encode);
+use List::Util qw(first);
+use REST::Client;
+use Try::Tiny;
+
+use SL::JSON;
+use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic (
+  'scalar --get_set_init' => [ qw(connector) ],
+);
+
+sub all_open_orders {
+  my ($self) = @_;
+
+  my $assoc = {
+              'associations' => {
+                'deliveries'   => {
+                  'associations' => {
+                    'shippingMethod' => [],
+                      'shippingOrderAddress' => {
+                        'associations' => {
+                                            'salutation'   => [],
+                                            'country'      => [],
+                                            'countryState' => []
+                                          }
+                                                }
+                                     }
+                                   }, # end deliveries
+                'language' => [],
+                'orderCustomer' => [],
+                'addresses' => {
+                  'associations' => {
+                                      'salutation'   => [],
+                                      'countryState' => [],
+                                      'country'      => []
+                                    }
+                                },
+                'tags' => [],
+                'lineItems' => {
+                  'associations' => {
+                    'product' => {
+                      'associations' => {
+                                          'tax' => []
+                                        }
+                                 }
+                                    }
+                                }, # end line items
+                'salesChannel' => [],
+                  'documents' => {          # currently not used
+                    'associations' => {
+                      'documentType' => []
+                                      }
+                                 },
+                'transactions' => {
+                  'associations' => {
+                    'paymentMethod' => []
+                                    }
+                                  },
+                'currency' => []
+            }, # end associations
+         'limit' => $self->config->orders_to_fetch ? $self->config->orders_to_fetch : undef,
+        # 'page' => 1,
+     'aggregations' => [
+                            {
+                              'field'      => 'billingAddressId',
+                              'definition' => 'order_address',
+                              'name'       => 'BillingAddress',
+                              'type'       => 'entity'
+                            }
+                          ],
+        'filter' => [
+                     {
+                        'value' => 'open', # open or completed (mind the past)
+                        'type' => 'equals',
+                        'field' => 'order.stateMachineState.technicalName'
+                      }
+                    ],
+        'total-count-mode' => 0
+      };
+  return $assoc;
+}
+
+# used for get_new_orders and get_one_order
+sub get_fetched_order_structure {
+  my ($self) = @_;
+  # set known params for the return structure
+  my %fetched_order  = (
+      shop_id          => $self->config->id,
+      shop_description => $self->config->description,
+      message          => '',
+      error            => '',
+      number_of_orders => 0,
+    );
+  return %fetched_order;
+}
+
+sub update_part {
+  my ($self, $shop_part, $todo) = @_;
+
+  #shop_part is passed as a param
+  croak t8("Need a valid Shop Part for updating Part") unless ref($shop_part) eq 'SL::DB::ShopPart';
+  croak t8("Invalid todo for updating Part")           unless $todo =~ m/(price|stock|price_stock|active|all)/;
+
+  my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
+  die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
+
+  my $tax_n_price = $shop_part->get_tax_and_price;
+  my $price       = $tax_n_price->{price};
+  my $taxrate     = $tax_n_price->{tax};
+
+  # simple calc for both cases, always give sw6 the calculated gross price
+  my ($net, $gross);
+  if ($self->config->pricetype eq 'brutto') {
+    $gross = $price;
+    $net   = $price / (1 + $taxrate/100);
+  } elsif ($self->config->pricetype eq 'netto') {
+    $net   = $price;
+    $gross = $price * (1 + $taxrate/100);
+  } else { die "Invalid state for price type"; }
+
+  my $update_p;
+  $update_p->{productNumber} = $part->partnumber;
+  $update_p->{name}          = _u8($part->description);
+  $update_p->{description}   =   $shop_part->shop->use_part_longdescription
+                               ? _u8($part->notes)
+                               : _u8($shop_part->shop_description);
+
+  # locales simple check for english
+  my $english = SL::DB::Manager::Language->get_first(query => [ description   => { ilike => 'Englisch' },
+                                                        or => [ template_code => { ilike => 'en' } ],
+                                                    ]);
+  if (ref $english eq 'SL::DB::Language') {
+    # add english translation for product
+    # TODO (or not): No Translations for shop_part->shop_description available
+    my $translation = first { $english->id == $_->language_id } @{ $part->translations };
+    $update_p->{translations}->{'en-GB'}->{name}        = _u8($translation->{translation});
+    $update_p->{translations}->{'en-GB'}->{description} = _u8($translation->{longdescription});
+  }
+
+  $update_p->{stock}  = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
+  # JSON::true JSON::false
+  # These special values become JSON true and JSON false values, respectively.
+  # You can also use \1 and \0 directly if you want
+  $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
+
+  # 1. check if there is already a product
+  my $product_filter = {
+          'filter' => [
+                        {
+                          'value' => $part->partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+  my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+
+  my $one_d; # maybe empty
+  try {
+    $one_d = from_json($ret->responseContent())->{data}->[0];
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+  # edit or create if not found
+  if ($one_d->{id}) {
+    #update
+    # we need price object structure and taxId
+    $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
+    if ($todo =~ m/(price|all)/) {
+      $update_p->{price}->[0]->{gross} = $gross;
+    }
+    undef $update_p->{partNumber}; # we dont need this one
+    $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
+    unless (204 == $ret->responseCode()) {
+      die t8('Part Description is too long for this Shopware version. It should have lower than 255 characters.')
+         if $ret->responseContent() =~ m/Diese Zeichenkette ist zu lang. Sie sollte.*255 Zeichen/;
+      die "Updating part with " .  $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+    }
+  } else {
+    # create part
+    # 1. get the correct tax for this product
+    my $tax_filter = {
+          'filter' => [
+                        {
+                          'value' => $taxrate,
+                          'type' => 'equals',
+                          'field' => 'taxRate'
+                        }
+                      ]
+        };
+    $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
+    die "Search for Tax with rate: " .  $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
+    try {
+      $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent();  };
+
+    # 2. get the correct currency for this product
+    my $currency_filter = {
+        'filter' => [
+                      {
+                        'value' => SL::DB::Default->get_default_currency,
+                        'type' => 'equals',
+                        'field' => 'isoCode'
+                      }
+                    ]
+      };
+    $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
+    die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
+      . $ret->responseContent() unless (200 == $ret->responseCode());
+
+    try {
+      $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent();  };
+
+    # 3. add net and gross price and allow variants
+    $update_p->{price}->[0]->{gross}  = $gross;
+    $update_p->{price}->[0]->{net}    = $net;
+    $update_p->{price}->[0]->{linked} = \1; # link product variants
+
+    $ret = $self->connector->POST('api/product', to_json($update_p));
+    die "Create for Product " .  $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+  }
+
+  # if there are images try to sync this with the shop_part
+  try {
+    $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
+  } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
+
+  # if there are categories try to sync this with the shop_part
+  try {
+    $self->sync_all_categories(shop_part => $shop_part);
+  } catch { die "Could not sync Categories for Part " . $part->partnumber . " Reason: $_" };
+
+  return 1; # no invalid response code -> success
+}
+sub sync_all_categories {
+  my ($self, %params) = @_;
+
+  my $shop_part = delete $params{shop_part};
+  croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+  my $partnumber = $shop_part->part->partnumber;
+  die "Shop Part but no kivi Partnumber" unless $partnumber;
+
+  my ($ret, $response_code);
+  # 1 get  uuid for product
+  my $product_filter = {
+          'filter' => [
+                        {
+                          'value' => $partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+
+  $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+  my ($product_id, $category_tree);
+  try {
+    $product_id    = from_json($ret->responseContent())->{data}->[0]->{id};
+    $category_tree = from_json($ret->responseContent())->{data}->[0]->{categoryIds};
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+  my $cat;
+  # if the part is connected to a category at all
+  if ($shop_part->shop_category) {
+    foreach my $row_cat (@{ $shop_part->shop_category }) {
+      $cat->{@{ $row_cat }[0]} = @{ $row_cat }[1];
+    }
+  }
+  # delete
+  foreach my $shopware_cat (@{ $category_tree }) {
+    if ($cat->{$shopware_cat}) {
+      # cat exists and no delete
+      delete $cat->{$shopware_cat};
+      next;
+    }
+    # cat exists and delete
+    $ret = $self->connector->DELETE("api/product/$product_id/categories/$shopware_cat");
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # now add only new categories
+  my $p;
+  $p->{id}  = $product_id;
+  $p->{categories} = ();
+  foreach my $new_cat (keys %{ $cat }) {
+    push @{ $p->{categories} }, {id => $new_cat};
+  }
+    $ret = $self->connector->PATCH("api/product/$product_id", to_json($p));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+}
+
+sub sync_all_images {
+  my ($self, %params) = @_;
+
+  $params{set_cover}       //= 1;
+  $params{delete_orphaned} //= 0;
+
+  my $shop_part = delete $params{shop_part};
+  croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+  my $partnumber = $shop_part->part->partnumber;
+  die "Shop Part but no kivi Partnumber" unless $partnumber;
+
+  my @upload_img  = $shop_part->get_images(want_binary => 1);
+
+  return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
+
+  my ($ret, $response_code);
+  # 1. get part uuid and get media associations
+  # 2. create or update the media entry for the filename
+  # 2.1 if no media entry exists create one
+  # 2.2 update file
+  # 2.2 create or update media_product and set position
+  # 3. optional set cover image
+  # 4. optional delete images in shopware which are not in kivi
+
+  # 1 get mediaid uuid for prodcut
+  my $product_filter = {
+              'associations' => {
+                'media'   => []
+              },
+          'filter' => [
+                        {
+                          'value' => $partnumber,
+                          'type'  => 'equals',
+                          'field' => 'productNumber'
+                        }
+                      ]
+    };
+
+  $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+  $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+  my ($product_id, $media_data);
+  try {
+    $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
+    # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
+  } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+  # 2 iterate all kivi images and save distinct name for later sync
+  my %existing_images;
+  foreach my $img (@upload_img) {
+    die $::locale->text("Need a image title") unless $img->{description};
+    my $distinct_media_name = $partnumber . '_' . $img->{description};
+    $existing_images{$distinct_media_name} = 1;
+    my $image_filter = {  'filter' => [
+                          {
+                            'value' => $distinct_media_name,
+                            'type'  => 'equals',
+                            'field' => 'fileName'
+                          }
+                        ]
+                      };
+    $ret           = $self->connector->POST('api/search/media', to_json($image_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $current_image_id; # maybe empty
+    try {
+      $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    # 2.1 no image with this title, create metadata for media and upload image
+    if (!$current_image_id) {
+      # not yet uploaded, create media entry
+      $ret = $self->connector->POST("/api/media?_response=true");
+      $response_code = $ret->responseCode();
+      die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+      try {
+        $current_image_id = from_json($ret->responseContent())->{data}{id};
+      } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+    }
+    # 2.2 update the image data (current_image_id was found or created)
+    $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
+                                    $img->{link},
+                                   {
+                                    "Content-Type"  => "image/$img->{extension}",
+                                   });
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+
+    # 2.3 check if a product media entry exists for this id
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        },
+                        {
+                          'value' => $current_image_id,
+                          'type' => 'equals',
+                          'field' => 'mediaId'
+                        },
+                      ]
+        };
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my ($has_product_media, $product_media_id);
+    try {
+      $has_product_media = from_json($ret->responseContent())->{total};
+      $product_media_id  = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    # 2.4 ... and either update or create the entry
+    #     set shopware position to kivi position
+    my $product_media;
+    $product_media->{position} = $img->{position}; # position may change
+
+    if ($has_product_media == 0) {
+      # 2.4.1 new entry. link product to media
+      $product_media->{productId} = $product_id;
+      $product_media->{mediaId}   = $current_image_id;
+      $ret = $self->connector->POST('api/product-media', to_json($product_media));
+    } elsif ($has_product_media == 1 && $product_media_id) {
+      $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
+    } else {
+      die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
+    }
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # 3. optional set image with position 1 as cover image
+  if ($params{set_cover}) {
+    # set cover if position == 1
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        },
+                        {
+                          'value' => '1',
+                          'type' => 'equals',
+                          'field' => 'position'
+                        },
+                          ]
+                             };
+
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $cover;
+    try {
+      $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+    $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+  }
+  # 4. optional delete orphaned images in shopware
+  if ($params{delete_orphaned}) {
+    # delete orphaned images
+    my $product_media_filter = {
+              'filter' => [
+                        {
+                          'value' => $product_id,
+                          'type' => 'equals',
+                          'field' => 'productId'
+                        }, ] };
+    $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
+    $response_code = $ret->responseCode();
+    die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+    my $img_ary;
+    try {
+      $img_ary = from_json($ret->responseContent())->{data};
+    } catch { die "Malformed JSON Data: $_ " . $ret->responseContent();  };
+
+    if (scalar @{ $img_ary} > 0) { # maybe no images at all
+      my %existing_img;
+      $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
+
+      while (my ($name, $id) = each %existing_img) {
+        next if $existing_images{$name};
+        $ret = $self->connector->DELETE("api/media/$id");
+        $response_code = $ret->responseCode();
+        die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+      }
+    }
+  }
+  return;
+}
+
+sub get_categories {
+  my ($self) = @_;
+
+  my $ret           = $self->connector->POST('api/search/category');
+  my $response_code = $ret->responseCode();
+
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+  my $import;
+  try {
+    $import = decode_json $ret->responseContent();
+  } catch {
+    die "Malformed JSON Data: $_ " . $ret->responseContent();
+  };
+
+  my @daten      = @{ $import->{data} };
+  my %categories = map { ($_->{id} => $_) } @daten;
+
+  my @categories_tree;
+  for (@daten) {
+    my $parent = $categories{$_->{parentId}};
+    if ($parent) {
+      $parent->{children} ||= [];
+      push @{ $parent->{children} }, $_;
+    } else {
+      push @categories_tree, $_;
+    }
+  }
+  return \@categories_tree;
+}
+
+sub get_one_order  {
+  my ($self, $ordnumber) = @_;
+
+  croak t8("No Order Number") unless $ordnumber;
+  # set known params for the return structure
+  my %fetched_order  = $self->get_fetched_order_structure;
+  my $assoc          = $self->all_open_orders();
+
+  # overwrite filter for exactly one ordnumber
+  $assoc->{filter}->[0]->{value} = $ordnumber;
+  $assoc->{filter}->[0]->{type}  = 'equals';
+  $assoc->{filter}->[0]->{field} = 'orderNumber';
+
+  # 1. fetch the order and import it as a kivi order
+  # 2. return the number of processed order (1)
+  my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
+
+  # 1. check for bad request or connection problems
+  if ($one_order->responseCode() != 200) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
+    return \%fetched_order;
+  }
+
+  # 1.1 parse json or exit
+  my $content;
+  try {
+    $content = from_json($one_order->responseContent());
+  } catch {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
+    return \%fetched_order;
+  };
+
+  # 2. check if we found ONE order at all
+  my $total = $content->{total};
+  if ($total == 0) {
+    $fetched_order{number_of_orders} = 0;
+    return \%fetched_order;
+  } elsif ($total != 1) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "More than one Order returned. Invalid State: $total";
+    return \%fetched_order;
+  }
+
+  # 3. there is one valid order, try to import this one
+  if ($self->import_data_to_shop_order($content->{data}->[0])) {
+    %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
+  } else {
+    $fetched_order{message} = "Error: $@";
+    $fetched_order{error}   = 1;
+  }
+  return \%fetched_order;
+}
+
+sub get_new_orders {
+  my ($self) = @_;
+
+  my %fetched_order  = $self->get_fetched_order_structure;
+  my $assoc          = $self->all_open_orders();
+
+  # 1. fetch all open orders and try to import it as a kivi order
+  # 2. return the number of processed order $total
+  my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
+
+  # 1. check for bad request or connection problems
+  if ($open_orders->responseCode() != 200) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
+    return \%fetched_order;
+  }
+
+  # 1.1 parse json or exit
+  my $content;
+  try {
+    $content = from_json($open_orders->responseContent());
+  } catch {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
+    return \%fetched_order;
+  };
+
+  # 2. check if we found one or more order at all
+  my $total = $content->{total};
+  if ($total == 0) {
+    $fetched_order{number_of_orders} = 0;
+    return \%fetched_order;
+  } elsif (!$total || !($total > 0)) {
+    $fetched_order{error}   = 1;
+    $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
+    return \%fetched_order;
+  }
+
+  # 3. there are open orders. try to import one by one
+  $fetched_order{number_of_orders} = 0;
+  foreach my $open_order (@{ $content->{data} }) {
+    if ($self->import_data_to_shop_order($open_order)) {
+      $fetched_order{number_of_orders}++;
+    } else {
+      $fetched_order{message} .= "Error at importing order with running number:"
+                                  . $fetched_order{number_of_orders}+1 . ": $@ \n";
+      $fetched_order{error}    = 1;
+    }
+  }
+  return \%fetched_order;
+}
+
+sub get_article {
+  my ($self, $partnumber) = @_;
+
+  $partnumber   = $::form->escape($partnumber);
+  my $product_filter = {
+              'filter' => [
+                            {
+                              'value' => $partnumber,
+                              'type' => 'equals',
+                              'field' => 'productNumber'
+                            }
+                          ]
+                       };
+  my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+  my $data_json;
+  try {
+    $data_json = decode_json $ret->responseContent();
+  } catch {
+    die "Malformed JSON Data: $_ " . $ret->responseContent();
+  };
+
+  # maybe no product was found ...
+  return undef unless scalar @{ $data_json->{data} } > 0;
+  # caller wants this structure:
+  # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
+  # $active_online = $shop_article->{data}->{active};
+  my $data;
+  $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
+  $data->{data}->{active}                = $data_json->{data}->[0]->{active};
+  return $data;
+}
+
+sub get_version {
+  my ($self) = @_;
+
+  my $return  = {}; # return for caller
+  my $ret     = {}; # internal return
+
+  #  1. check if we can connect at all
+  #  2. request version number
+
+  $ret = $self->connector;
+  if (200 != $ret->responseCode()) {
+    $return->{success}         = 0;
+    $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
+    return $return;
+  }
+
+  $ret = $self->connector->GET('api/_info/version');
+  if (200 == $ret->responseCode()) {
+    my $version = from_json($self->connector->responseContent())->{version};
+    $return->{success}         = 1;
+    $return->{data}->{version} = $version;
+  } else {
+    $return->{success}         = 0;
+    $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
+  }
+
+  return $return;
+}
+
+sub set_orderstatus {
+  my ($self, $order_id, $transition) = @_;
+
+  croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id   =~ m/^[0-9a-f]{32}$/;
+  croak "NO valid transition value"                     unless $transition =~ m/(open|process|cancel|complete)/;
+  my $ret;
+  $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
+  my $response_code = $ret->responseCode();
+  die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
+
+}
+
+sub init_connector {
+  my ($self) = @_;
+
+  my $protocol = $self->config->server =~ /(^https:\/\/|^http:\/\/)/ ? '' : $self->config->protocol . '://';
+  my $client   = REST::Client->new(host => $protocol . $self->config->server);
+
+  $client->getUseragent()->proxy([$self->config->protocol], $self->config->proxy) if $self->config->proxy;
+  $client->addHeader('Content-Type', 'application/json');
+  $client->addHeader('charset',      'UTF-8');
+  $client->addHeader('Accept',       'application/json');
+
+  my %auth_req = (
+                   client_id     => $self->config->login,
+                   client_secret => $self->config->password,
+                   grant_type    => "client_credentials",
+                 );
+
+  my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
+
+  unless (200 == $ret->responseCode()) {
+    $self->{errors} .= $ret->responseContent();
+    return;
+  }
+
+  my $token = from_json($client->responseContent())->{access_token};
+  unless ($token) {
+    $self->{errors} .= "No Auth-Token received";
+    return;
+  }
+  # persist refresh token
+  $client->addHeader('Authorization' => 'Bearer ' . $token);
+  return $client;
+}
+
+sub import_data_to_shop_order {
+  my ($self, $import) = @_;
+
+  # failsafe checks for not yet implemented
+  die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
+
+  # no mapping unless we also have at least one shop order item ...
+  my $order_pos = delete $import->{lineItems};
+  croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
+
+  my $shop_order = $self->map_data_to_shoporder($import);
+
+  my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
+    $shop_order->save;
+    my $id = $shop_order->id;
+
+    my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
+    my $position = 0;
+    my $active_price_source = $self->config->price_source;
+    #Mapping Positions
+    foreach my $pos (@positions) {
+      $position++;
+      my $price       = $::form->round_amount($pos->{unitPrice}, 2); # unit
+      my %pos_columns = ( description          => $pos->{product}->{name},
+                          partnumber           => $pos->{product}->{productNumber},
+                          price                => $price,
+                          quantity             => $pos->{quantity},
+                          position             => $position,
+                          tax_rate             => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
+                          shop_trans_id        => $pos->{id}, # pos id or shop_trans_id ? or dont care?
+                          shop_order_id        => $id,
+                          active_price_source  => $active_price_source,
+                        );
+      my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
+      $pos_insert->save;
+    }
+    $shop_order->positions($position);
+
+    if ( $self->config->shipping_costs_parts_id ) {
+      die t8("Not yet implemented");
+      # TODO NOT YET Implemented nor tested, this is shopware5 code:
+      my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
+      my %shipping_pos = ( description    => $import->{data}->{dispatch}->{name},
+                           partnumber     => $shipping_part->partnumber,
+                           price          => $import->{data}->{invoiceShipping},
+                           quantity       => 1,
+                           position       => $position,
+                           shop_trans_id  => 0,
+                           shop_order_id  => $id,
+                         );
+      my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
+      $shipping_pos_insert->save;
+    }
+
+    my $customer = $shop_order->get_customer;
+
+    if (ref $customer eq 'SL::DB::Customer') {
+      $shop_order->kivi_customer_id($customer->id);
+    }
+    $shop_order->save;
+
+    # update state in shopware before transaction ends
+    $self->set_orderstatus($shop_order->shop_trans_id, "process");
+
+    1;
+
+  }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
+                $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
+}
+
+sub map_data_to_shoporder {
+  my ($self, $import) = @_;
+
+  croak "Expect a hash with one order." unless ref $import eq 'HASH';
+  # we need one number and a order date, some total prices and one customer
+  croak "Does not look like a shopware6 order" unless    $import->{orderNumber}
+                                                      && $import->{orderDateTime}
+                                                      && ref $import->{price} eq 'HASH'
+                                                      && ref $import->{orderCustomer} eq 'HASH';
+
+  my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
+  die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
+
+  my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} }       @{ $import->{addresses} } ];
+  my $shipto_ary  = [ grep { $_->{id} == $shipto_id }                        @{ $import->{addresses} } ];
+  my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} }        @{ $import->{paymentMethods} } ];
+
+  die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
+          $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
+    unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
+
+  my $billing = $billing_ary->[0];
+  my $shipto  = $shipto_ary->[0];
+  # TODO payment info is not used at all
+  my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
+
+  # check mandatory fields from shopware
+  die t8("No billing city")   unless $billing->{city};
+  die t8("No shipto city")    unless $shipto->{city};
+  die t8("No customer email") unless $import->{orderCustomer}->{email};
+
+  # extract order date
+  my $parser = DateTime::Format::Strptime->new(pattern   => '%Y-%m-%dT%H:%M:%S',
+                                               locale    => 'de_DE',
+                                               time_zone => 'local'             );
+  my $orderdate;
+  try {
+    $orderdate = $parser->parse_datetime($import->{orderDateTime});
+  } catch { die "Cannot parse Order Date" . $_ };
+
+  my $shop_id      = $self->config->id;
+  my $tax_included = $self->config->pricetype;
+
+  # TODO copied from shopware5 connector
+  # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
+  my %payment_ids_methods = (
+    # shopware_paymentId => kivitendo_payment_id
+  );
+  my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
+  my $default_payment_id = $default_payment ? $default_payment->id : undef;
+  #
+
+
+  my %columns = (
+    amount                  => $import->{amountTotal},
+    billing_city            => $billing->{city},
+    billing_company         => $billing->{company},
+    billing_country         => $billing->{country}->{name},
+    billing_department      => $billing->{department},
+    billing_email           => $import->{orderCustomer}->{email},
+    billing_fax             => $billing->{fax},
+    billing_firstname       => $billing->{firstName},
+    #billing_greeting        => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    billing_lastname        => $billing->{lastName},
+    billing_phone           => $billing->{phone},
+    billing_street          => $billing->{street},
+    billing_vat             => $billing->{vatId},
+    billing_zipcode         => $billing->{zipcode},
+    customer_city           => $billing->{city},
+    customer_company        => $billing->{company},
+    customer_country        => $billing->{country}->{name},
+    customer_department     => $billing->{department},
+    customer_email          => $billing->{email},
+    customer_fax            => $billing->{fax},
+    customer_firstname      => $billing->{firstName},
+    #customer_greeting       => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    customer_lastname       => $billing->{lastName},
+    customer_phone          => $billing->{phoneNumber},
+    customer_street         => $billing->{street},
+    customer_vat            => $billing->{vatId},
+    customer_zipcode        => $billing->{zipcode},
+#    customer_newsletter     => $customer}->{newsletter},
+    delivery_city           => $shipto->{city},
+    delivery_company        => $shipto->{company},
+    delivery_country        => $shipto->{country}->{name},
+    delivery_department     => $shipto->{department},
+    delivery_email          => "",
+    delivery_fax            => $shipto->{fax},
+    delivery_firstname      => $shipto->{firstName},
+    #delivery_greeting       => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
+    delivery_lastname       => $shipto->{lastName},
+    delivery_phone          => $shipto->{phone},
+    delivery_street         => $shipto->{street},
+    delivery_vat            => $shipto->{vatId},
+    delivery_zipcode        => $shipto->{zipCode},
+#    host                    => $shop}->{hosts},
+    netamount               => $import->{amountNet},
+    order_date              => $orderdate,
+    payment_description     => $payment->{name},
+    payment_id              => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
+    tax_included            => $tax_included eq "brutto" ? 1 : 0,
+    shop_ordernumber        => $import->{orderNumber},
+    shop_id                 => $shop_id,
+    shop_trans_id           => $import->{id},
+    # TODO map these:
+    #remote_ip               => $import->{remoteAddress},
+    #sepa_account_holder     => $import->{paymentIntances}->{accountHolder},
+    #sepa_bic                => $import->{paymentIntances}->{bic},
+    #sepa_iban               => $import->{paymentIntances}->{iban},
+    #shipping_costs          => $import->{invoiceShipping},
+    #shipping_costs_net      => $import->{invoiceShippingNet},
+    #shop_c_billing_id       => $import->{billing}->{customerId},
+    #shop_c_billing_number   => $import->{billing}->{number},
+    #shop_c_delivery_id      => $import->{shipping}->{id},
+    #shop_customer_id        => $import->{customerId},
+    #shop_customer_number    => $import->{billing}->{number},
+    #shop_customer_comment   => $import->{customerComment},
+  );
+
+  my $shop_order = SL::DB::ShopOrder->new(%columns);
+  return $shop_order;
+}
+
+sub _u8 {
+  my ($value) = @_;
+  return encode('UTF-8', $value // '');
+}
+
+1;
+
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+  SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
+
+=head1 SYNOPSIS
+
+
+=head1 DESCRIPTION
+
+=head1 AVAILABLE METHODS
+
+=over 4
+
+=item C<get_one_order>
+
+=item C<get_new_orders>
+
+=item C<update_part>
+
+Updates all metadata for a shop part. See base class for a general description.
+Specific Implementation notes:
+=over 4
+
+=item Calls sync_all_images with set_cover = 1 and delete_orphaned = 1
+
+=item Checks if longdescription should be taken from part or shop_part
+
+=item Checks if a language with the name 'Englisch' or template_code 'en'
+      is available and sets the shopware6 'en-GB' locales for the product
+
+=item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
+
+The connecting key for shopware to kivi images is the image name.
+To get distinct entries the kivi partnumber is combined with the title (description)
+of the image. Therefore part1000_someTitlefromUser should be unique in
+Shopware.
+All image data is simply send to shopware whether or not image data
+has been edited recently.
+If set_cover is set, the image with the position 1 will be used as
+the shopware cover image.
+If delete_orphaned ist set, all images related to the shopware product
+which are not also in kivitendo will be deleted.
+Shopware (6.4.x) takes care of deleting all the relations if the media
+entry for the image is deleted.
+More on media and Shopware6 can be found here:
+https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
+
+=back
+
+=over 4
+
+=item C<get_article>
+
+=item C<get_categories>
+
+=item C<get_version>
+
+Tries to establish a connection and in a second step
+tries to get the server's version number.
+Returns a hashref with the data structure the Base class expects.
+
+=item C<set_orderstatus>
+
+=item C<init_connector>
+
+Inits the connection to the REST Server.
+Errors are collected in $self->{errors} and undef will be returned.
+If successful returns a REST::Client object for further communications.
+
+=back
+
+=head1 SEE ALSO
+
+L<SL::ShopConnector::ALL>
+
+=head1 BUGS
+
+None yet. :)
+
+=head1 TODOS
+
+=over 4
+
+=item * Map all data to shop_order
+
+Missing fields are commented in the sub map_data_to_shoporder.
+Some items are SEPA debit info, IP adress, delivery costs etc
+Furthermore Shopware6 uses currency, country and locales information.
+Detailed list:
+
+    #customer_newsletter     => $customer}->{newsletter},
+    #remote_ip               => $import->{remoteAddress},
+    #sepa_account_holder     => $import->{paymentIntances}->{accountHolder},
+    #sepa_bic                => $import->{paymentIntances}->{bic},
+    #sepa_iban               => $import->{paymentIntances}->{iban},
+    #shipping_costs          => $import->{invoiceShipping},
+    #shipping_costs_net      => $import->{invoiceShippingNet},
+    #shop_c_billing_id       => $import->{billing}->{customerId},
+    #shop_c_billing_number   => $import->{billing}->{number},
+    #shop_c_delivery_id      => $import->{shipping}->{id},
+    #shop_customer_id        => $import->{customerId},
+    #shop_customer_number    => $import->{billing}->{number},
+    #shop_customer_comment   => $import->{customerComment},
+
+=item * Use shipping_costs_parts_id for additional shipping costs
+
+Currently dies if a shipping_costs_parts_id is set in the config
+
+=item * Payment Infos can be read from shopware but is not linked with kivi
+
+Unused data structures in sub map_data_to_shoporder => payment_ary
+
+=item * Delete orphaned images is new in this connector, but should be in a separate method
+
+=item * Fetch from last order number is ignored and should not be needed
+
+Fetch orders also sets the state of the order from open to process. The state setting
+is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
+at all. Nevertheless get_one_order just gets one order with the exactly matching order number
+and ignores any shopware order transition state.
+
+=item * Get one order and get new orders is basically the same except for the filter
+
+Right now the returning structure and the common parts of the filter are in two separate functions
+
+=item * Locales!
+
+Many error messages are thrown, but at least the more common cases should be localized.
+
+=item * Multi language support
+
+By guessing the correct german name for the english language some translation for parts can
+also be synced. This should be more clear (language configuration for shops) and the order
+synchronisation should also handle this (longdescription is simply copied from part.notes)
+
+=back
+
+=head1 AUTHOR
+
+Jan Büren jan@kivitendo.de
+
+=cut
index 64a1e46..56d524c 100644 (file)
@@ -38,7 +38,7 @@ namespace('kivi.ShopPart', function(ns) {
     });
   }
 
-  ns.add_shop_part = function(part_id,shop_id) {
+  ns.add_shop_part = function() {
     var form = $('form').serializeArray();
     form.push( { name: 'action', value: 'ShopPart/update' }
     );
index 1f8d087..29feb69 100755 (executable)
@@ -579,6 +579,7 @@ $self->{texts} = {
   'Cannot delete transaction!'  => 'Buchung kann nicht gelöscht werden!',
   'Cannot delete vendor!'       => 'Lieferant kann nicht gelöscht werden!',
   'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.' => 'Konnte keine passende Vorlage für diesen Druckauftrag finden. Bitte benachrichtigen Sie Ihren Vorlagenadministrator. Die folgenden Pfade wurden durchsucht: #1 ',
+  'Cannot get shippingOrderAddressId for #1' => 'Finde das Feld shippingOrderAddressId für #1 nicht.',
   'Cannot have a value in both Debit and Credit!' => 'Es kann nicht gleichzeitig Soll und Haben gebucht werden!',
   'Cannot post Payment!'        => 'Zahlung kann nicht gebucht werden!',
   'Cannot post Receipt!'        => 'Beleg kann nicht gebucht werden!',
@@ -1374,6 +1375,7 @@ $self->{texts} = {
   'Error when saving: #1'       => 'Fehler beim Speichern: #1',
   'Error while applying year-end bookings!' => 'Fehler beim Durchführen der Abschlußbuchungen!',
   'Error while creating project with project number of new order number, project number #1 already exists!' => 'Fehler beim Erstellen eines Projekts mit der Projektnummer der neuen Auftragsnummer, Projektnummer #1 existiert bereits!',
+  'Error while saving shop order #1. DB Error #2. Generic exception #3.' => 'Fehler beim Speichern der Shop-Bestellung #1. DB Fehler #2. Genereller Fehler #3.',
   'Error with default taxzone'  => 'Ungültige Standardsteuerzone',
   'Error!'                      => 'Fehler!',
   'Error: #1'                   => 'Fehler: #1',
@@ -1524,6 +1526,7 @@ $self->{texts} = {
   'Feb'                         => 'Feb',
   'February'                    => 'Februar',
   'Fee'                         => 'Gebühr',
+  'Fetch from last order number is not implemented' => 'Das Abholen ab der letzten Auftragsnummer ist nicht implementiert',
   'Fetch order'                 => 'Hole Bestellung',
   'Field'                       => 'Feld',
   'File'                        => 'Datei',
@@ -1840,6 +1843,7 @@ $self->{texts} = {
   'Invalid follow-up ID.'       => 'Ungültige Wiedervorlage-ID.',
   'Invalid quantity.'           => 'Die Mengenangabe ist ungültig.',
   'Invalid request type \'#1\'' => 'Ungültiger Request-Typ \'#1\'',
+  'Invalid todo for updating Part' => 'Ungültiger Wert für das Feld todo bei Artikel aktualisieren',
   'Invalid transactions'        => 'Ungültige Buchungen',
   'Invalid variable #1'         => 'Ungültige Variable #1',
   'Invdate'                     => 'Rechnungsdatum',
@@ -2120,6 +2124,8 @@ $self->{texts} = {
   'Name does not make sense without any bsooqr options' => 'Option "Name in gewählten Belegen" wird ignoriert.',
   'Name in Selected Records'    => 'Name in gewählten Belegen',
   'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => 'Name des Ziel- oder Quellkontos (wenn die Spalten remote_name und remote_name_1 existieren werden diese zu Feld "remote_name" zusammengefügt)',
+  'Need a image title'          => 'Benötige einen Titel für das Bild',
+  'Need a valid Shop Part for updating Part' => 'Benötige eine gültiges Shop Part Objekt, um den Artikel zu aktualisieren.',
   'Need at least one original position for the workflow Order to Delivery Order!' => 'Benötige mindestens eine Position die aus dem Auftrag übernommen wurde, ansonsten ist der Workflow inkosistent.',
   'Need charge number!'         => 'Benötige Chargennummer!',
   'Negative reductions are possible to model price increases.' => 'Negative Abschläge sind möglich um Aufschläge zu modellieren.',
@@ -2160,11 +2166,14 @@ $self->{texts} = {
   'No 1:n or n:1 relation'      => 'Keine 1:n oder n:1 Beziehung',
   'No AP Record Template for this vendor found, please add one' => 'Konnte keine Kreditorenbuchungsvorlage für diesen Lieferanten finden, bitte legen Sie eine an.',
   'No AP template was found.'   => 'Keine Kreditorenbuchungsvorlage gefunden.',
+  'No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3' => 'Keine Rechnungs- und Lieferadresse zur Bestellnummer #1 mit Rechnungs-ID #2 und Liefer-ID #3 gefunden',
   'No Company Address given'    => 'Keine Firmenadresse hinterlegt!',
   'No Company Name given'       => 'Kein Firmenname hinterlegt!',
   'No Customer was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Endkunde gefunden',
   'No GL template was found.'   => 'Keine Dialogbuchungsvorlage gefunden.',
   'No Journal'                  => 'Kein Journal',
+  'No Order Number'             => 'Keine Auftragsnummer',
+  'No Order items fetched'      => 'Keine Auftragspositionen gefunden',
   'No Shopdescription'          => 'Keine Shop-Artikelbeschreibung',
   'No Shopimages'               => 'Keine Shop-Bilder',
   'No VAT Info for this Factur-X/ZUGFeRD invoice, please ask your vendor to add this for his Factur-X/ZUGFeRD data.' => 'Konnte keine UST-ID für diese Factur-X-/ZUGFeRD-Rechnungen finden, bitte fragen Sie bei Ihren Lieferanten nach, ob dieses Feld im Factur-X-/ZUGFeRD-Datensatz gesetzt wird.',
@@ -2181,6 +2190,7 @@ $self->{texts} = {
   'No bank account flagged for QRBill usage was found.' => 'Kein Bankkonto markiert für QR-Rechnung gefunden.',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => 'Für diesen Kunden wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => 'Für diesen Lieferanten wurden in seinen Stammdaten keine Kontodaten hinterlegt. Solange dies nicht geschehen ist, können Sie keine Überweisungen für den Lieferanten anlegen.',
+  'No billing city'             => 'Die Stadt für die Rechnungsadresse fehlt',
   'No bins have been added to this warehouse yet.' => 'Es wurden zu diesem Lager noch keine Lagerplätze angelegt.',
   'No carry-over chart configured!' => 'Kein Saldenvortragskonto konfiguriert!',
   'No changes since previous version.' => 'Keine Änderungen seit der letzten Version.',
@@ -2188,6 +2198,7 @@ $self->{texts} = {
   'No contact selected to delete' => 'Keine Ansprechperson zum Löschen ausgewählt',
   'No contra account selected!' => 'Kein Gegenkonto ausgewählt!',
   'No custom data exports have been created yet.' => 'Es wurden noch keine benutzerdefinierten Datenexporte angelegt.',
+  'No customer email'           => 'Die E-Mail-Adresse des Kunden fehlt',
   'No customer has been selected yet.' => 'Es wurde noch kein Kunde ausgewählt.',
   'No customer selected or found!' => 'Kein Kunde selektiert oder keinen gefunden!',
   'No data was found.'          => 'Es wurden keine Daten gefunden.',
@@ -2230,6 +2241,7 @@ $self->{texts} = {
   'No sections created yet'     => 'Keine Abschnitte erstellt',
   'No sections have been created so far.' => 'Bisher wurden noch keine Abschnitte angelegt.',
   'No sections have been created yet.' => 'Es wurden noch keine Abschnitte angelegt.',
+  'No shipto city'              => 'Die Stadt für die Lieferadresse fehlt',
   'No shipto selected to delete' => 'Keine Lieferadresse zum Löschen ausgewählt',
   'No start date given, setting to #1' => 'Kein Startdatum gegeben, setze Startdatum auf #1',
   'No such job #1 in the database.' => 'Hintergrund-Job #1 existiert nicht mehr.',
@@ -2265,6 +2277,7 @@ $self->{texts} = {
   'Not done yet'                => 'Noch nicht fertig',
   'Not enough in stock for the serial number #1' => 'Nicht genug auf Lager von der Seriennummer #1',
   'Not obsolete'                => 'Gültig',
+  'Not yet implemented'         => 'Noch nicht implementiert',
   'Note'                        => 'Hinweis',
   'Note that parameter names must not be quoted.' => 'Beachten Sie, dass Parameternamen nicht in Anführungszeichen stehen dürfen.',
   'Note: Taxkeys must have a "valid from" date, and will not behave correctly without.' => 'Hinweis: Steuerschlüssel sind fehlerhaft ohne "Gültig ab" Datum',
@@ -2367,6 +2380,7 @@ $self->{texts} = {
   'Orders'                      => 'Aufträge',
   'Orders / Delivery Orders deleteable' => 'Aufträge / Lieferscheine löschbar',
   'Orders to fetch'             => 'Anzahl Bestellungen holen',
+  'Orders to fetch neeeds a positive Integer' => 'Die Anzahl der zu holenden Aufträge muss eine positive Ganzzahl sein',
   'Orientation'                 => 'Seitenformat',
   'Orig. Size w/h'              => 'Orig. Größe b/h',
   'Origin of personal data'     => 'Herkunft der personenbezogenen Daten',
@@ -2418,6 +2432,7 @@ $self->{texts} = {
   'Part (typeabbreviation)'     => 'W',
   'Part Classification'         => 'Artikel-Klassifizierung',
   'Part Description'            => 'Artikelbeschreibung',
+  'Part Description is too long for this Shopware version. It should have lower than 255 characters.' => 'Artikelbeschreibung enthält mehr als 255 Zeichen. Shopware in dieser Version kann nur Artikelbeschreibungen mit weniger als 255 Zeichen verarbeiten.',
   'Part Description missing!'   => 'Artikelbezeichnung fehlt!',
   'Part Notes'                  => 'Bemerkungen',
   'Part Number'                 => 'Artikelnummer',
@@ -2689,6 +2704,7 @@ $self->{texts} = {
   'Proposal'                    => 'Vorschlag',
   'Proposals'                   => 'Vorschläge',
   'Protocol'                    => 'Protokoll',
+  'Proxy'                       => 'Proxy',
   'Prozentual/Absolut'          => 'Prozentual/Absolut',
   'Purchase'                    => 'Einkauf',
   'Purchase (typeabbreviation)' => 'E',
@@ -3087,6 +3103,8 @@ $self->{texts} = {
   'Shipping Address'            => 'Lieferadresse',
   'Shipping Point'              => 'Versandort',
   'Shipping address (name)'     => 'Name der Lieferadresse',
+  'Shipping cost article is not implemented' => 'Versandkosten-Artikel ist nicht implementiert',
+  'Shipping cost article not implemented' => 'Lieferkosten-Artikel nicht implementiert',
   'Shipping costs'              => 'Versandkosten',
   'Shipping date'               => 'Lieferdatum',
   'Shippingcosts'               => 'Versandkosten',
@@ -3412,6 +3430,8 @@ $self->{texts} = {
   'The Factur-X/ZUGFeRD version used is not supported.' => 'Die verwendete Factur-X-/ZUGFeRD-Version wird nicht unterstützt.',
   'The GL transaction #1 has been deleted.' => 'Die Dialogbuchung #1 wurde gelöscht.',
   'The Geierlein path has not been set in the configuration.' => 'Der Geierlein-Pfad wurde in der Konfigurationsdatei nicht gesetzt.',
+  'The Host Name is missing'    => 'Der Name des Servers fehlt',
+  'The Host Name seems invalid' => 'Der Name des Servers sieht ungültig aus, bspw.: www.server.com',
   'The IBAN \'#1\' is not valid as IBANs in #2 must be exactly #3 characters long.' => 'Die IBAN \'#1\' ist ungültig, da IBANs in #2 genau #3 Zeichen lang sein müssen.',
   'The IBAN is missing.'        => 'Die IBAN fehlt.',
   'The ID #1 is not a valid database ID.' => 'Die ID #1 ist keine gültige Datenbank-ID.',
@@ -3420,6 +3440,8 @@ $self->{texts} = {
   'The PDF has been created'    => 'Die PDF-Datei wurde erstellt.',
   'The PDF has been previewed'  => 'PDF-Druckvorschau ausgeführt',
   'The PDF has been printed'    => 'Das PDF-Dokument wurde gedruckt.',
+  'The Protocol for Host Name seems invalid (expected: http:// or https://)!' => 'Das Protokoll für den Server sieht falsch aus. Erwartet wird "http://" oder "https://".',
+  'The Proxy Name seems invalid' => 'Der Hostname des Proxys sieht falsch aus',
   'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt',
   'The SEPA strings have been saved.' => 'Die bei SEPA-Überweisungen verwendeten Begriffe wurden gespeichert.',
   'The SQL query can be parameterized with variables named as follows: <%name%>.' => 'Die SQL-Abfrage kann mittels Variablen wie folgt parametrisiert werden: <%Variablenname%>.',
@@ -3917,6 +3939,7 @@ $self->{texts} = {
   'Transaction'                 => 'Buchung',
   'Transaction %d cancelled.'   => 'Buchung %d erfolgreich storniert.',
   'Transaction Date missing!'   => 'Buchungsdatum fehlt!',
+  'Transaction Description is not yet implemented' => 'Vorgangsbezeichnung ist noch nicht implementiert',
   'Transaction ID missing.'     => 'Die Buchungs-ID fehlt.',
   'Transaction Value'           => 'Umsatz',
   'Transaction Value Currency Code' => 'WKZ Umsatz',
@@ -4020,7 +4043,6 @@ $self->{texts} = {
   'Until'                       => 'Bis',
   'Update'                      => 'Erneuern',
   'Update Discount'             => 'Rabatt übernehmen',
-  'Update Partnumber'           => 'Update Artikel',
   'Update Price'                => 'Preis übernehmen',
   'Update Prices'               => 'Preise aktualisieren',
   'Update SKR04: new tax account 3804 (19%)' => 'Update SKR04: neues Steuerkonto 3804 (19%) für innergemeinschaftlichen Erwerb',
@@ -4065,6 +4087,8 @@ $self->{texts} = {
   'Use File Storage backend'    => 'Verwende Dateisystem-Backend',
   'Use Filemanagement'          => 'Verwende Dateimanagement',
   'Use Income'                  => 'GUV und BWA verwenden',
+  'Use Long Description from Parts for Shop Long Description' => 'Verwende den Artikel Langtext aus den Stammdaten für den Langtext im Shop',
+  'Use Long Description from Parts is only for Shopware6 implemented' => 'Der Langtext aus den Stammdaten kann nur in Shopware6 verwendet werden',
   'Use UStVA'                   => 'UStVA verwenden',
   'Use WebDAV Repository'       => 'Verwende WebDAV',
   'Use WebDAV Storage backend'  => 'Verwende WebDAV-Backend',
@@ -4144,7 +4168,6 @@ $self->{texts} = {
   'Version'                     => 'Version',
   'Version actions'             => 'Aktionen für Versionen',
   'Version number'              => 'Versionsnummer',
-  'Version: '                   => 'Version',
   'Versions'                    => 'Versionen',
   'View SEPA export'            => 'SEPA-Export-Details ansehen',
   'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
index aabb43c..79e8f6b 100644 (file)
@@ -579,6 +579,7 @@ $self->{texts} = {
   'Cannot delete transaction!'  => '',
   'Cannot delete vendor!'       => '',
   'Cannot find matching template for this print request. Please contact your template maintainer. I tried these: #1.' => '',
+  'Cannot get shippingOrderAddressId for #1' => '',
   'Cannot have a value in both Debit and Credit!' => '',
   'Cannot post Payment!'        => '',
   'Cannot post Receipt!'        => '',
@@ -1374,6 +1375,7 @@ $self->{texts} = {
   'Error when saving: #1'       => '',
   'Error while applying year-end bookings!' => '',
   'Error while creating project with project number of new order number, project number #1 already exists!' => '',
+  'Error while saving shop order #1. DB Error #2. Generic exception #3.' => '',
   'Error with default taxzone'  => '',
   'Error!'                      => '',
   'Error: #1'                   => '',
@@ -1524,6 +1526,7 @@ $self->{texts} = {
   'Feb'                         => '',
   'February'                    => '',
   'Fee'                         => '',
+  'Fetch from last order number is not implemented' => '',
   'Fetch order'                 => '',
   'Field'                       => '',
   'File'                        => '',
@@ -1840,6 +1843,7 @@ $self->{texts} = {
   'Invalid follow-up ID.'       => '',
   'Invalid quantity.'           => '',
   'Invalid request type \'#1\'' => '',
+  'Invalid todo for updating Part' => '',
   'Invalid transactions'        => '',
   'Invalid variable #1'         => '',
   'Invdate'                     => '',
@@ -2120,6 +2124,8 @@ $self->{texts} = {
   'Name does not make sense without any bsooqr options' => '',
   'Name in Selected Records'    => '',
   'Name of the goal/source (if field names remote_name and remote_name_1 exist they will be combined into field "remote_name")' => '',
+  'Need a image title'          => '',
+  'Need a valid Shop Part for updating Part' => '',
   'Need at least one original position for the workflow Order to Delivery Order!' => '',
   'Need charge number!'         => '',
   'Negative reductions are possible to model price increases.' => '',
@@ -2160,11 +2166,14 @@ $self->{texts} = {
   'No 1:n or n:1 relation'      => '',
   'No AP Record Template for this vendor found, please add one' => '',
   'No AP template was found.'   => '',
+  'No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3' => '',
   'No Company Address given'    => '',
   'No Company Name given'       => '',
   'No Customer was found matching the search parameters.' => '',
   'No GL template was found.'   => '',
   'No Journal'                  => '',
+  'No Order Number'             => '',
+  'No Order items fetched'      => '',
   'No Shopdescription'          => '',
   'No Shopimages'               => '',
   'No VAT Info for this Factur-X/ZUGFeRD invoice, please ask your vendor to add this for his Factur-X/ZUGFeRD data.' => '',
@@ -2181,6 +2190,7 @@ $self->{texts} = {
   'No bank account flagged for QRBill usage was found.' => '',
   'No bank information has been entered in this customer\'s master data entry. You cannot create bank collections unless you enter bank information.' => '',
   'No bank information has been entered in this vendor\'s master data entry. You cannot create bank transfers unless you enter bank information.' => '',
+  'No billing city'             => '',
   'No bins have been added to this warehouse yet.' => '',
   'No carry-over chart configured!' => '',
   'No changes since previous version.' => '',
@@ -2188,6 +2198,7 @@ $self->{texts} = {
   'No contact selected to delete' => '',
   'No contra account selected!' => '',
   'No custom data exports have been created yet.' => '',
+  'No customer email'           => '',
   'No customer has been selected yet.' => '',
   'No customer selected or found!' => '',
   'No data was found.'          => '',
@@ -2230,6 +2241,7 @@ $self->{texts} = {
   'No sections created yet'     => '',
   'No sections have been created so far.' => '',
   'No sections have been created yet.' => '',
+  'No shipto city'              => '',
   'No shipto selected to delete' => '',
   'No start date given, setting to #1' => '',
   'No such job #1 in the database.' => '',
@@ -2265,6 +2277,7 @@ $self->{texts} = {
   'Not done yet'                => '',
   'Not enough in stock for the serial number #1' => '',
   'Not obsolete'                => '',
+  'Not yet implemented'         => '',
   'Note'                        => '',
   'Note that parameter names must not be quoted.' => '',
   'Note: Taxkeys must have a "valid from" date, and will not behave correctly without.' => '',
@@ -2367,6 +2380,7 @@ $self->{texts} = {
   'Orders'                      => '',
   'Orders / Delivery Orders deleteable' => '',
   'Orders to fetch'             => '',
+  'Orders to fetch neeeds a positive Integer' => '',
   'Orientation'                 => '',
   'Orig. Size w/h'              => '',
   'Origin of personal data'     => '',
@@ -3087,6 +3101,8 @@ $self->{texts} = {
   'Shipping Address'            => '',
   'Shipping Point'              => '',
   'Shipping address (name)'     => '',
+  'Shipping cost article is not implemented' => '',
+  'Shipping cost article not implemented' => '',
   'Shipping costs'              => '',
   'Shipping date'               => '',
   'Shippingcosts'               => '',
@@ -3411,6 +3427,8 @@ $self->{texts} = {
   'The Factur-X/ZUGFeRD version used is not supported.' => '',
   'The GL transaction #1 has been deleted.' => '',
   'The Geierlein path has not been set in the configuration.' => '',
+  'The Host Name is missing'    => '',
+  'The Host Name seems invalid' => '',
   'The IBAN \'#1\' is not valid as IBANs in #2 must be exactly #3 characters long.' => '',
   'The IBAN is missing.'        => '',
   'The ID #1 is not a valid database ID.' => '',
@@ -3916,6 +3934,7 @@ $self->{texts} = {
   'Transaction'                 => '',
   'Transaction %d cancelled.'   => '',
   'Transaction Date missing!'   => '',
+  'Transaction Description is not yet implemented' => '',
   'Transaction ID missing.'     => '',
   'Transaction Value'           => '',
   'Transaction Value Currency Code' => '',
@@ -4064,6 +4083,8 @@ $self->{texts} = {
   'Use File Storage backend'    => '',
   'Use Filemanagement'          => '',
   'Use Income'                  => 'Use GUV and BWA',
+  'Use Long Description from Parts for Shop Long Description' => '',
+  'Use Long Description from Parts is only for Shopware6 implemented' => '',
   'Use UStVA'                   => '',
   'Use WebDAV Repository'       => '',
   'Use WebDAV Storage backend'  => '',
@@ -4143,7 +4164,6 @@ $self->{texts} = {
   'Version'                     => '',
   'Version actions'             => '',
   'Version number'              => '',
-  'Version: '                   => '',
   'Versions'                    => '',
   'View SEPA export'            => '',
   'View background job execution result' => '',
diff --git a/sql/Pg-upgrade2/shop_orders_update_4.sql b/sql/Pg-upgrade2/shop_orders_update_4.sql
new file mode 100644 (file)
index 0000000..93324fb
--- /dev/null
@@ -0,0 +1,8 @@
+-- @tag: shop_orders_update_4
+-- @description: Ändern der Tabellen shop_orders, shop_trans_id darf auch Text enthalten
+-- @depends: shop_orders_update_1 shop_orders_update_2 shop_orders_update_3
+
+-- @ignore: 0
+
+ALTER TABLE shop_orders ALTER COLUMN shop_trans_id TYPE text;
+ALTER TABLE shop_order_items ALTER COLUMN shop_trans_id TYPE text;
diff --git a/sql/Pg-upgrade2/shops_5.sql b/sql/Pg-upgrade2/shops_5.sql
new file mode 100644 (file)
index 0000000..72b4355
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shops_5
+-- @description: Shop-Config um Option zur direkten Beschreibungsübernahme erweitern
+-- @depends: shop_4
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN use_part_longdescription BOOLEAN default false;
diff --git a/sql/Pg-upgrade2/shops_6.sql b/sql/Pg-upgrade2/shops_6.sql
new file mode 100644 (file)
index 0000000..788bdb9
--- /dev/null
@@ -0,0 +1,6 @@
+-- @tag: shop_add_proxy
+-- @description: Shop-Config um Option Proxy erweitert
+-- @depends: shops_5
+-- @ignore: 0
+
+ALTER TABLE shops ADD COLUMN proxy TEXT default '';
index 051a230..3c65b4d 100644 (file)
    <th>[% LxERP.t8("Action") %]</th>
   </tr>
   </thead>
-  [% # L.dump(SELF.part) %]
+  [% L.dump(SELF.part) %]
   [%- FOREACH shop_part = SELF.part.shop_parts %]
   [% IF !shop_part.shop.obsolete %]
+
   <tr class="listrow">
    <td>[% HTML.escape( shop_part.shop.description ) %]</td>
    <td>[% L.html_tag('span', shop_part.active, id => 'shop_part_active_' _ shop_part.id ) %]</td>
-   <td>[% L.html_tag('span', shop_part.shop_description, id => 'shop_part_description_' _ shop_part.id ) %]</td>
+   <td>
+    [% IF shop_part.shop.use_part_longdescription %]
+      [% L.html_tag('span', shop_part.part.notes, id => 'shop_part_description_' _ shop_part.id ) %]
+    [% ELSE %]
+      [% L.html_tag('span', shop_part.shop_description, id => 'shop_part_description_' _ shop_part.id ) %]
+    [% END %]
+  </td>
    <td>[% L.html_tag('span',LxERP.t8(), id => 'active_price_source_' _ shop_part.id) %] </td>
    <td>[% L.html_tag('span','Price', id => 'price_' _ shop_part.id) %]</td>
    <td>[% L.html_tag('span','Stock', id => 'stock_' _ shop_part.id) %]</td>
index 9d51997..55e6628 100644 (file)
@@ -15,7 +15,7 @@
           <li>
           [% checked = '' %]
           [% FOREACH cat_row = SELF.shop_part.shop_category %]
-            [% IF cat_row.0 == categorie.id %]
+            [% IF (cat_row.0 == categorie.id) || (SELF.shop_part.shop.connector == 'shopware6' && cat_row == categorie.id) %]
               [% checked = 'checked' %]
             [% END %]
           [% END %]
index 5536ad7..5379532 100644 (file)
     [%- L.hidden_tag("shop_part.part_id", FORM.part_id) %]
     [% END %]
 
+  [% # L.dump(SELF.shop_part.shop) %]
     <table>
     <tr>
      <td>[% LxERP.t8("Description") %]</td>
-     <td colspan="3">[% L.textarea_tag('shop_part.shop_description', SELF.shop_part.shop_description, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]</td>
+     <td colspan="3">
+       [% IF SELF.shop_part.shop.use_part_longdescription %]
+         [% L.textarea_tag('notes', SELF.shop_part.part.notes, wrap="soft", readonly="readonly", style="width: 350px; height: 150px", class="texteditor") %]
+       [% ELSE %]
+         [% L.textarea_tag('shop_part.shop_description', SELF.shop_part.shop_description, wrap="soft", style="width: 350px; height: 150px", class="texteditor") %]
+       [% END %]
+     </td>
     </tr>
     <tr>
      <td>[% LxERP.t8("Active") %]</td>
      <td>[% L.textarea_tag("shop_part.metatag_description", SELF.shop_part.metatag_description, rows=4) %]</td>
     </tr>
     </table>
-
     [% IF SELF.shop_part.id %]
-    [% L.button_tag("kivi.ShopPart.save_shop_part(" _ SELF.shop_part.id _ ")", LxERP.t8("Save"))  %]</td>
+      [% L.button_tag("kivi.ShopPart.save_shop_part(" _ SELF.shop_part.id _ ")", LxERP.t8("Save"))  %]</td>
     [% ELSE %]
-    [% L.button_tag("kivi.ShopPart.add_shop_part(" _ FORM.part_id _", " _ FORM.shop_id _")", LxERP.t8("Save"))  %]</td>
+      [% L.button_tag("kivi.ShopPart.add_shop_part()", LxERP.t8("Save"))  %]</td>
     [% END %]
-    [% # L.button_tag("kivi.ShopPart.update_partnumber()", LxERP.t8("Update Partnumber"))  %]</td>
-
-    [% # L.hidden_tag("action", "ShopPart/dispatch") %]
-    [% # L.submit_tag('action_update', LxERP.t8('Save')) %]
-
-
   </div>
 </form>
 
index cda1e0b..2f08e72 100644 (file)
     <th align="right">[% 'Port' | $T8 %]</th>
     <td>[%- L.input_tag("shop.port", SELF.shop.port, size=5) %]</td>
   </tr>
+  <tr>
+    <th align="right">[% 'Proxy' | $T8 %]</th>
+    <td>[%- L.input_tag("shop.proxy", SELF.shop.proxy, size=size) %]</td>
+  </tr>
   <tr>
     <th align="right">[% 'Path' | $T8 %]</th>
     <td>[%- L.input_tag("shop.path", SELF.shop.path, size=size) %]</td>
     <th align="right">[% 'Obsolete' | $T8 %]</th>
     <td>[% L.checkbox_tag('shop.obsolete', checked = SELF.shop.obsolete, for_submit=1) %]</td>
   </tr>
+  <tr>
+    <th align="right">[% 'Use Long Description from Parts for Shop Long Description' | $T8 %]</th>
+    <td>[% L.yes_no_tag('shop.use_part_longdescription', SELF.shop.use_part_longdescription) %]</td>
+  </tr>
 </table>
 
  <hr>
index f7033a6..65e23a3 100644 (file)
@@ -2,7 +2,7 @@
 [%- IF ok %]
 
  <p class="message_ok">[% LxERP.t8('The connection to the shop was established successfully.') %]</p>
- <p>[% LxERP.t8('Version: ')%][% HTML.escape(version) %]</p>
+ <p>[% LxERP.t8('Version')%]: [% HTML.escape(version) %]</p>
 
 [%- ELSE %]