X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FShopConnector%2FShopware6.pm;h=6d04a1b338851d7344a608d938f169265db52ffc;hb=cfb029307ecd356f377952f3190df655cbd447c6;hp=e065176e8ada540294dc1d04ff404c827dc55e73;hpb=fba387a443dc89647cdbdce7d7c11f42c7b62f23;p=kivitendo-erp.git diff --git a/SL/ShopConnector/Shopware6.pm b/SL/ShopConnector/Shopware6.pm index e065176e8..6d04a1b33 100644 --- a/SL/ShopConnector/Shopware6.pm +++ b/SL/ShopConnector/Shopware6.pm @@ -6,11 +6,13 @@ 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) ], @@ -105,21 +107,12 @@ sub update_part { my ($self, $shop_part, $todo) = @_; #shop_part is passed as a param - croak "Need a valid Shop Part for updating Part" unless ref($shop_part) eq 'SL::DB::ShopPart'; - croak "Invalid todo for updating Part" unless $todo =~ m/(price|stock|price_stock|active|all)/; + 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 @cat = (); - # if the part is connected to a category at all - if ($shop_part->shop_category) { - foreach my $row_cat ( @{ $shop_part->shop_category } ) { - my $temp = { ( id => @{$row_cat}[0] ) }; - push ( @cat, $temp ); - } - } - my $tax_n_price = $shop_part->get_tax_and_price; my $price = $tax_n_price->{price}; my $taxrate = $tax_n_price->{tax}; @@ -136,7 +129,22 @@ sub update_part { my $update_p; $update_p->{productNumber} = $part->partnumber; - $update_p->{name} = $part->description; + $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 @@ -172,7 +180,11 @@ sub update_part { } undef $update_p->{partNumber}; # we dont need this one $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p)); - die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode()); + 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 @@ -223,8 +235,72 @@ sub update_part { $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) = @_; @@ -453,7 +529,7 @@ sub get_categories { sub get_one_order { my ($self, $ordnumber) = @_; - die "No ordnumber" unless $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(); @@ -581,6 +657,9 @@ sub get_article { } 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}; @@ -622,8 +701,11 @@ sub get_version { 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)/; + # one state differs + $transition = 'complete' if $transition eq 'completed'; + + croak "No shop 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(); @@ -634,7 +716,10 @@ sub set_orderstatus { sub init_connector { my ($self) = @_; - my $client = REST::Client->new(host => $self->config->server); + 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'); @@ -666,11 +751,11 @@ sub import_data_to_shop_order { my ($self, $import) = @_; # failsafe checks for not yet implemented - die $::locale->text('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id; + 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("No Order items fetched") unless ref $order_pos eq 'ARRAY'; + croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY'; my $shop_order = $self->map_data_to_shoporder($import); @@ -685,8 +770,8 @@ sub import_data_to_shop_order { foreach my $pos (@positions) { $position++; my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit - my %pos_columns = ( description => $pos->{product}->{description}, - partnumber => $pos->{label}, + my %pos_columns = ( description => $pos->{product}->{name}, + partnumber => $pos->{product}->{productNumber}, price => $price, quantity => $pos->{quantity}, position => $position, @@ -701,7 +786,7 @@ sub import_data_to_shop_order { $shop_order->positions($position); if ( $self->config->shipping_costs_parts_id ) { - die "Not yet implemented"; + 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}, @@ -728,8 +813,8 @@ sub import_data_to_shop_order { 1; - }) || die ('error while saving shop order ' . $shop_order->{shop_ordernumber} . 'Error: ' . $shop_order->db->error . "\n" . - 'generic exception:' . $@); + }) || 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 { @@ -743,14 +828,14 @@ sub map_data_to_shoporder { && ref $import->{orderCustomer} eq 'HASH'; my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId}; - die "Cannot get shippingOrderAddressId for $import->{orderNumber}" unless $shipto_id; + 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} } ]; - croak("No Billing and ship to address, for Order Number " . $import->{orderNumber} . - "ID Billing:" . $import->{billingAddressId} . " ID Shipping $shipto_id ") + 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]; @@ -758,9 +843,10 @@ sub map_data_to_shoporder { # TODO payment info is not used at all my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef; - croak "No billing city" unless $billing->{city}; - croak "No shipto city" unless $shipto->{city}; - croak "No customer email" unless $import->{orderCustomer}->{email}; + # 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', @@ -774,6 +860,7 @@ sub map_data_to_shoporder { 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 @@ -883,10 +970,21 @@ __END__ =item C +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 -The important key for shopware is the image name. To get distinct -entries the kivi partnumber is combined with the title (description) +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 @@ -900,6 +998,9 @@ 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 @@ -938,6 +1039,21 @@ None yet. :) 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 @@ -964,6 +1080,12 @@ Right now the returning structure and the common parts of the filter are in two 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