1 package SL::ShopConnector::Shopware6;
5 use parent qw(SL::ShopConnector::Base);
9 use List::Util qw(first);
14 use SL::Helper::Flash;
15 use SL::Locale::String qw(t8);
17 use Rose::Object::MakeMethods::Generic (
18 'scalar --get_set_init' => [ qw(connector) ],
28 'shippingMethod' => [],
29 'shippingOrderAddress' => {
39 'orderCustomer' => [],
58 'documents' => { # currently not used
70 'limit' => $self->config->orders_to_fetch ? $self->config->orders_to_fetch : undef,
74 'field' => 'billingAddressId',
75 'definition' => 'order_address',
76 'name' => 'BillingAddress',
82 'value' => 'open', # open or completed (mind the past)
84 'field' => 'order.stateMachineState.technicalName'
87 'total-count-mode' => 0
92 # used for get_new_orders and get_one_order
93 sub get_fetched_order_structure {
95 # set known params for the return structure
97 shop_id => $self->config->id,
98 shop_description => $self->config->description,
101 number_of_orders => 0,
103 return %fetched_order;
107 my ($self, $shop_part, $todo) = @_;
109 #shop_part is passed as a param
110 croak t8("Need a valid Shop Part for updating Part") unless ref($shop_part) eq 'SL::DB::ShopPart';
111 croak t8("Invalid todo for updating Part") unless $todo =~ m/(price|stock|price_stock|active|all)/;
113 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
114 die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
117 # if the part is connected to a category at all
118 if ($shop_part->shop_category) {
119 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
120 my $temp = { ( id => @{$row_cat}[0] ) };
121 push ( @cat, $temp );
125 my $tax_n_price = $shop_part->get_tax_and_price;
126 my $price = $tax_n_price->{price};
127 my $taxrate = $tax_n_price->{tax};
129 # simple calc for both cases, always give sw6 the calculated gross price
131 if ($self->config->pricetype eq 'brutto') {
133 $net = $price / (1 + $taxrate/100);
134 } elsif ($self->config->pricetype eq 'netto') {
136 $gross = $price * (1 + $taxrate/100);
137 } else { die "Invalid state for price type"; }
140 $update_p->{productNumber} = $part->partnumber;
141 $update_p->{name} = _u8($part->description);
142 $update_p->{description} = $shop_part->shop->use_part_longdescription
144 : _u8($shop_part->shop_description);
146 # locales simple check for english
147 my $english = SL::DB::Manager::Language->get_first(query => [ description => { ilike => 'Englisch' },
148 or => [ template_code => { ilike => 'en' } ],
150 if (ref $english eq 'SL::DB::Language') {
151 # add english translation for product
152 # TODO (or not): No Translations for shop_part->shop_description available
153 my $translation = first { $english->id == $_->language_id } @{ $part->translations };
154 $update_p->{translations}->{'en-GB'}->{name} = _u8($translation->{translation});
155 $update_p->{translations}->{'en-GB'}->{description} = _u8($translation->{longdescription});
158 $update_p->{stock} = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
159 # JSON::true JSON::false
160 # These special values become JSON true and JSON false values, respectively.
161 # You can also use \1 and \0 directly if you want
162 $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
164 # 1. check if there is already a product
165 my $product_filter = {
168 'value' => $part->partnumber,
170 'field' => 'productNumber'
174 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
175 my $response_code = $ret->responseCode();
176 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
178 my $one_d; # maybe empty
180 $one_d = from_json($ret->responseContent())->{data}->[0];
181 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
182 # edit or create if not found
185 # we need price object structure and taxId
186 $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
187 if ($todo =~ m/(price|all)/) {
188 $update_p->{price}->[0]->{gross} = $gross;
190 undef $update_p->{partNumber}; # we dont need this one
191 $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
192 die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
195 # 1. get the correct tax for this product
205 $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
206 die "Search for Tax with rate: " . $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
208 $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
209 } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent(); };
211 # 2. get the correct currency for this product
212 my $currency_filter = {
215 'value' => SL::DB::Default->get_default_currency,
221 $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
222 die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
223 . $ret->responseContent() unless (200 == $ret->responseCode());
226 $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
227 } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent(); };
229 # 3. add net and gross price and allow variants
230 $update_p->{price}->[0]->{gross} = $gross;
231 $update_p->{price}->[0]->{net} = $net;
232 $update_p->{price}->[0]->{linked} = \1; # link product variants
234 $ret = $self->connector->POST('api/product', to_json($update_p));
235 die "Create for Product " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
238 # if there are images try to sync this with the shop_part
240 $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
241 } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
243 return 1; # no invalid response code -> success
246 sub sync_all_images {
247 my ($self, %params) = @_;
249 $params{set_cover} //= 1;
250 $params{delete_orphaned} //= 0;
252 my $shop_part = delete $params{shop_part};
253 croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
255 my $partnumber = $shop_part->part->partnumber;
256 die "Shop Part but no kivi Partnumber" unless $partnumber;
258 my @upload_img = $shop_part->get_images(want_binary => 1);
260 return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
262 my ($ret, $response_code);
263 # 1. get part uuid and get media associations
264 # 2. create or update the media entry for the filename
265 # 2.1 if no media entry exists create one
267 # 2.2 create or update media_product and set position
268 # 3. optional set cover image
269 # 4. optional delete images in shopware which are not in kivi
271 # 1 get mediaid uuid for prodcut
272 my $product_filter = {
278 'value' => $partnumber,
280 'field' => 'productNumber'
285 $ret = $self->connector->POST('api/search/product', to_json($product_filter));
286 $response_code = $ret->responseCode();
287 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
288 my ($product_id, $media_data);
290 $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
291 # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
292 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
294 # 2 iterate all kivi images and save distinct name for later sync
296 foreach my $img (@upload_img) {
297 die $::locale->text("Need a image title") unless $img->{description};
298 my $distinct_media_name = $partnumber . '_' . $img->{description};
299 $existing_images{$distinct_media_name} = 1;
300 my $image_filter = { 'filter' => [
302 'value' => $distinct_media_name,
304 'field' => 'fileName'
308 $ret = $self->connector->POST('api/search/media', to_json($image_filter));
309 $response_code = $ret->responseCode();
310 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
311 my $current_image_id; # maybe empty
313 $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
314 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
316 # 2.1 no image with this title, create metadata for media and upload image
317 if (!$current_image_id) {
318 # not yet uploaded, create media entry
319 $ret = $self->connector->POST("/api/media?_response=true");
320 $response_code = $ret->responseCode();
321 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
323 $current_image_id = from_json($ret->responseContent())->{data}{id};
324 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
326 # 2.2 update the image data (current_image_id was found or created)
327 $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
330 "Content-Type" => "image/$img->{extension}",
332 $response_code = $ret->responseCode();
333 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
335 # 2.3 check if a product media entry exists for this id
336 my $product_media_filter = {
339 'value' => $product_id,
341 'field' => 'productId'
344 'value' => $current_image_id,
350 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
351 $response_code = $ret->responseCode();
352 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
353 my ($has_product_media, $product_media_id);
355 $has_product_media = from_json($ret->responseContent())->{total};
356 $product_media_id = from_json($ret->responseContent())->{data}->[0]->{id};
357 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
359 # 2.4 ... and either update or create the entry
360 # set shopware position to kivi position
362 $product_media->{position} = $img->{position}; # position may change
364 if ($has_product_media == 0) {
365 # 2.4.1 new entry. link product to media
366 $product_media->{productId} = $product_id;
367 $product_media->{mediaId} = $current_image_id;
368 $ret = $self->connector->POST('api/product-media', to_json($product_media));
369 } elsif ($has_product_media == 1 && $product_media_id) {
370 $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
372 die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
374 $response_code = $ret->responseCode();
375 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
377 # 3. optional set image with position 1 as cover image
378 if ($params{set_cover}) {
379 # set cover if position == 1
380 my $product_media_filter = {
383 'value' => $product_id,
385 'field' => 'productId'
390 'field' => 'position'
395 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
396 $response_code = $ret->responseCode();
397 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
400 $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
401 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
402 $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
403 $response_code = $ret->responseCode();
404 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
406 # 4. optional delete orphaned images in shopware
407 if ($params{delete_orphaned}) {
408 # delete orphaned images
409 my $product_media_filter = {
412 'value' => $product_id,
414 'field' => 'productId'
416 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
417 $response_code = $ret->responseCode();
418 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
421 $img_ary = from_json($ret->responseContent())->{data};
422 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
424 if (scalar @{ $img_ary} > 0) { # maybe no images at all
426 $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
428 while (my ($name, $id) = each %existing_img) {
429 next if $existing_images{$name};
430 $ret = $self->connector->DELETE("api/media/$id");
431 $response_code = $ret->responseCode();
432 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
442 my $ret = $self->connector->POST('api/search/category');
443 my $response_code = $ret->responseCode();
445 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
449 $import = decode_json $ret->responseContent();
451 die "Malformed JSON Data: $_ " . $ret->responseContent();
454 my @daten = @{ $import->{data} };
455 my %categories = map { ($_->{id} => $_) } @daten;
459 my $parent = $categories{$_->{parentId}};
461 $parent->{children} ||= [];
462 push @{ $parent->{children} }, $_;
464 push @categories_tree, $_;
467 return \@categories_tree;
471 my ($self, $ordnumber) = @_;
473 croak t8("No Order Number") unless $ordnumber;
474 # set known params for the return structure
475 my %fetched_order = $self->get_fetched_order_structure;
476 my $assoc = $self->all_open_orders();
478 # overwrite filter for exactly one ordnumber
479 $assoc->{filter}->[0]->{value} = $ordnumber;
480 $assoc->{filter}->[0]->{type} = 'equals';
481 $assoc->{filter}->[0]->{field} = 'orderNumber';
483 # 1. fetch the order and import it as a kivi order
484 # 2. return the number of processed order (1)
485 my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
487 # 1. check for bad request or connection problems
488 if ($one_order->responseCode() != 200) {
489 $fetched_order{error} = 1;
490 $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
491 return \%fetched_order;
494 # 1.1 parse json or exit
497 $content = from_json($one_order->responseContent());
499 $fetched_order{error} = 1;
500 $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
501 return \%fetched_order;
504 # 2. check if we found ONE order at all
505 my $total = $content->{total};
507 $fetched_order{number_of_orders} = 0;
508 return \%fetched_order;
509 } elsif ($total != 1) {
510 $fetched_order{error} = 1;
511 $fetched_order{message} = "More than one Order returned. Invalid State: $total";
512 return \%fetched_order;
515 # 3. there is one valid order, try to import this one
516 if ($self->import_data_to_shop_order($content->{data}->[0])) {
517 %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
519 $fetched_order{message} = "Error: $@";
520 $fetched_order{error} = 1;
522 return \%fetched_order;
528 my %fetched_order = $self->get_fetched_order_structure;
529 my $assoc = $self->all_open_orders();
531 # 1. fetch all open orders and try to import it as a kivi order
532 # 2. return the number of processed order $total
533 my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
535 # 1. check for bad request or connection problems
536 if ($open_orders->responseCode() != 200) {
537 $fetched_order{error} = 1;
538 $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
539 return \%fetched_order;
542 # 1.1 parse json or exit
545 $content = from_json($open_orders->responseContent());
547 $fetched_order{error} = 1;
548 $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
549 return \%fetched_order;
552 # 2. check if we found one or more order at all
553 my $total = $content->{total};
555 $fetched_order{number_of_orders} = 0;
556 return \%fetched_order;
557 } elsif (!$total || !($total > 0)) {
558 $fetched_order{error} = 1;
559 $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
560 return \%fetched_order;
563 # 3. there are open orders. try to import one by one
564 $fetched_order{number_of_orders} = 0;
565 foreach my $open_order (@{ $content->{data} }) {
566 if ($self->import_data_to_shop_order($open_order)) {
567 $fetched_order{number_of_orders}++;
569 $fetched_order{message} .= "Error at importing order with running number:"
570 . $fetched_order{number_of_orders}+1 . ": $@ \n";
571 $fetched_order{error} = 1;
574 return \%fetched_order;
578 my ($self, $partnumber) = @_;
580 $partnumber = $::form->escape($partnumber);
581 my $product_filter = {
584 'value' => $partnumber,
586 'field' => 'productNumber'
590 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
592 my $response_code = $ret->responseCode();
593 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
597 $data_json = decode_json $ret->responseContent();
599 die "Malformed JSON Data: $_ " . $ret->responseContent();
602 # maybe no product was found ...
603 return undef unless scalar @{ $data_json->{data} } > 0;
604 # caller wants this structure:
605 # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
606 # $active_online = $shop_article->{data}->{active};
608 $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
609 $data->{data}->{active} = $data_json->{data}->[0]->{active};
616 my $return = {}; # return for caller
617 my $ret = {}; # internal return
619 # 1. check if we can connect at all
620 # 2. request version number
622 $ret = $self->connector;
623 if (200 != $ret->responseCode()) {
624 $return->{success} = 0;
625 $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
629 $ret = $self->connector->GET('api/_info/version');
630 if (200 == $ret->responseCode()) {
631 my $version = from_json($self->connector->responseContent())->{version};
632 $return->{success} = 1;
633 $return->{data}->{version} = $version;
635 $return->{success} = 0;
636 $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
642 sub set_orderstatus {
643 my ($self, $order_id, $transition) = @_;
645 croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
646 croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
648 $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
649 my $response_code = $ret->responseCode();
650 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
657 my $client = REST::Client->new(host => $self->config->server);
658 $client->addHeader('Content-Type', 'application/json');
659 $client->addHeader('charset', 'UTF-8');
660 $client->addHeader('Accept', 'application/json');
663 client_id => $self->config->login,
664 client_secret => $self->config->password,
665 grant_type => "client_credentials",
668 my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
670 unless (200 == $ret->responseCode()) {
671 $self->{errors} .= $ret->responseContent();
675 my $token = from_json($client->responseContent())->{access_token};
677 $self->{errors} .= "No Auth-Token received";
680 # persist refresh token
681 $client->addHeader('Authorization' => 'Bearer ' . $token);
685 sub import_data_to_shop_order {
686 my ($self, $import) = @_;
688 # failsafe checks for not yet implemented
689 die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
691 # no mapping unless we also have at least one shop order item ...
692 my $order_pos = delete $import->{lineItems};
693 croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
695 my $shop_order = $self->map_data_to_shoporder($import);
697 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
699 my $id = $shop_order->id;
701 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
703 my $active_price_source = $self->config->price_source;
705 foreach my $pos (@positions) {
707 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
708 my %pos_columns = ( description => $pos->{product}->{description},
709 partnumber => $pos->{label},
711 quantity => $pos->{quantity},
712 position => $position,
713 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
714 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
715 shop_order_id => $id,
716 active_price_source => $active_price_source,
718 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
721 $shop_order->positions($position);
723 if ( $self->config->shipping_costs_parts_id ) {
724 die t8("Not yet implemented");
725 # TODO NOT YET Implemented nor tested, this is shopware5 code:
726 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
727 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
728 partnumber => $shipping_part->partnumber,
729 price => $import->{data}->{invoiceShipping},
731 position => $position,
733 shop_order_id => $id,
735 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
736 $shipping_pos_insert->save;
739 my $customer = $shop_order->get_customer;
741 if (ref $customer eq 'SL::DB::Customer') {
742 $shop_order->kivi_customer_id($customer->id);
746 # update state in shopware before transaction ends
747 $self->set_orderstatus($shop_order->shop_trans_id, "process");
751 }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
752 $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
755 sub map_data_to_shoporder {
756 my ($self, $import) = @_;
758 croak "Expect a hash with one order." unless ref $import eq 'HASH';
759 # we need one number and a order date, some total prices and one customer
760 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
761 && $import->{orderDateTime}
762 && ref $import->{price} eq 'HASH'
763 && ref $import->{orderCustomer} eq 'HASH';
765 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
766 die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
768 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
769 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
770 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
772 die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
773 $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
774 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
776 my $billing = $billing_ary->[0];
777 my $shipto = $shipto_ary->[0];
778 # TODO payment info is not used at all
779 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
781 # check mandatory fields from shopware
782 die t8("No billing city") unless $billing->{city};
783 die t8("No shipto city") unless $shipto->{city};
784 die t8("No customer email") unless $import->{orderCustomer}->{email};
787 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
789 time_zone => 'local' );
792 $orderdate = $parser->parse_datetime($import->{orderDateTime});
793 } catch { die "Cannot parse Order Date" . $_ };
795 my $shop_id = $self->config->id;
796 my $tax_included = $self->config->pricetype;
798 # TODO copied from shopware5 connector
799 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
800 my %payment_ids_methods = (
801 # shopware_paymentId => kivitendo_payment_id
803 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
804 my $default_payment_id = $default_payment ? $default_payment->id : undef;
809 amount => $import->{amountTotal},
810 billing_city => $billing->{city},
811 billing_company => $billing->{company},
812 billing_country => $billing->{country}->{name},
813 billing_department => $billing->{department},
814 billing_email => $import->{orderCustomer}->{email},
815 billing_fax => $billing->{fax},
816 billing_firstname => $billing->{firstName},
817 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
818 billing_lastname => $billing->{lastName},
819 billing_phone => $billing->{phone},
820 billing_street => $billing->{street},
821 billing_vat => $billing->{vatId},
822 billing_zipcode => $billing->{zipcode},
823 customer_city => $billing->{city},
824 customer_company => $billing->{company},
825 customer_country => $billing->{country}->{name},
826 customer_department => $billing->{department},
827 customer_email => $billing->{email},
828 customer_fax => $billing->{fax},
829 customer_firstname => $billing->{firstName},
830 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
831 customer_lastname => $billing->{lastName},
832 customer_phone => $billing->{phoneNumber},
833 customer_street => $billing->{street},
834 customer_vat => $billing->{vatId},
835 customer_zipcode => $billing->{zipcode},
836 # customer_newsletter => $customer}->{newsletter},
837 delivery_city => $shipto->{city},
838 delivery_company => $shipto->{company},
839 delivery_country => $shipto->{country}->{name},
840 delivery_department => $shipto->{department},
841 delivery_email => "",
842 delivery_fax => $shipto->{fax},
843 delivery_firstname => $shipto->{firstName},
844 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
845 delivery_lastname => $shipto->{lastName},
846 delivery_phone => $shipto->{phone},
847 delivery_street => $shipto->{street},
848 delivery_vat => $shipto->{vatId},
849 delivery_zipcode => $shipto->{zipCode},
850 # host => $shop}->{hosts},
851 netamount => $import->{amountNet},
852 order_date => $orderdate,
853 payment_description => $payment->{name},
854 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
855 tax_included => $tax_included eq "brutto" ? 1 : 0,
856 shop_ordernumber => $import->{orderNumber},
858 shop_trans_id => $import->{id},
860 #remote_ip => $import->{remoteAddress},
861 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
862 #sepa_bic => $import->{paymentIntances}->{bic},
863 #sepa_iban => $import->{paymentIntances}->{iban},
864 #shipping_costs => $import->{invoiceShipping},
865 #shipping_costs_net => $import->{invoiceShippingNet},
866 #shop_c_billing_id => $import->{billing}->{customerId},
867 #shop_c_billing_number => $import->{billing}->{number},
868 #shop_c_delivery_id => $import->{shipping}->{id},
869 #shop_customer_id => $import->{customerId},
870 #shop_customer_number => $import->{billing}->{number},
871 #shop_customer_comment => $import->{customerComment},
874 my $shop_order = SL::DB::ShopOrder->new(%columns);
880 return encode('UTF-8', $value // '');
891 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
898 =head1 AVAILABLE METHODS
902 =item C<get_one_order>
904 =item C<get_new_orders>
908 Updates all metadata for a shop part. See base class for a general description.
909 Specific Implementation notes:
912 =item * Calls sync_all_images with set_cover = 1 and delete_orphaned = 1
914 =item * Checks if longdescription should be taken from part or shop_part
916 =item * Checks if a language with the name 'Englisch' or template_code 'en'
917 is available and sets the shopware6 'en-GB' locales for the product
921 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
923 The connecting key for shopware to kivi images is the image name.
924 To get distinct entries the kivi partnumber is combined with the title (description)
925 of the image. Therefore part1000_someTitlefromUser should be unique in
927 All image data is simply send to shopware whether or not image data
928 has been edited recently.
929 If set_cover is set, the image with the position 1 will be used as
930 the shopware cover image.
931 If delete_orphaned ist set, all images related to the shopware product
932 which are not also in kivitendo will be deleted.
933 Shopware (6.4.x) takes care of deleting all the relations if the media
934 entry for the image is deleted.
935 More on media and Shopware6 can be found here:
936 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
941 =item C<get_categories>
945 Tries to establish a connection and in a second step
946 tries to get the server's version number.
947 Returns a hashref with the data structure the Base class expects.
949 =item C<set_orderstatus>
951 =item C<init_connector>
953 Inits the connection to the REST Server.
954 Errors are collected in $self->{errors} and undef will be returned.
955 If successful returns a REST::Client object for further communications.
961 L<SL::ShopConnector::ALL>
971 =item * Map all data to shop_order
973 Missing fields are commented in the sub map_data_to_shoporder.
974 Some items are SEPA debit info, IP adress, delivery costs etc
975 Furthermore Shopware6 uses currency, country and locales information.
978 #customer_newsletter => $customer}->{newsletter},
979 #remote_ip => $import->{remoteAddress},
980 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
981 #sepa_bic => $import->{paymentIntances}->{bic},
982 #sepa_iban => $import->{paymentIntances}->{iban},
983 #shipping_costs => $import->{invoiceShipping},
984 #shipping_costs_net => $import->{invoiceShippingNet},
985 #shop_c_billing_id => $import->{billing}->{customerId},
986 #shop_c_billing_number => $import->{billing}->{number},
987 #shop_c_delivery_id => $import->{shipping}->{id},
988 #shop_customer_id => $import->{customerId},
989 #shop_customer_number => $import->{billing}->{number},
990 #shop_customer_comment => $import->{customerComment},
992 =item * Use shipping_costs_parts_id for additional shipping costs
994 Currently dies if a shipping_costs_parts_id is set in the config
996 =item * Payment Infos can be read from shopware but is not linked with kivi
998 Unused data structures in sub map_data_to_shoporder => payment_ary
1000 =item * Delete orphaned images is new in this connector, but should be in a separate method
1002 =item * Fetch from last order number is ignored and should not be needed
1004 Fetch orders also sets the state of the order from open to process. The state setting
1005 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
1006 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
1007 and ignores any shopware order transition state.
1009 =item * Get one order and get new orders is basically the same except for the filter
1011 Right now the returning structure and the common parts of the filter are in two separate functions
1015 Many error messages are thrown, but at least the more common cases should be localized.
1021 Jan Büren jan@kivitendo.de