1 package SL::ShopConnector::Shopware6;
5 use parent qw(SL::ShopConnector::Base);
13 use SL::Helper::Flash;
14 use SL::Locale::String qw(t8);
16 use Rose::Object::MakeMethods::Generic (
17 'scalar --get_set_init' => [ qw(connector) ],
27 'shippingMethod' => [],
28 'shippingOrderAddress' => {
38 'orderCustomer' => [],
57 'documents' => { # currently not used
69 'limit' => $self->config->orders_to_fetch ? $self->config->orders_to_fetch : undef,
73 'field' => 'billingAddressId',
74 'definition' => 'order_address',
75 'name' => 'BillingAddress',
81 'value' => 'open', # open or completed (mind the past)
83 'field' => 'order.stateMachineState.technicalName'
86 'total-count-mode' => 0
91 # used for get_new_orders and get_one_order
92 sub get_fetched_order_structure {
94 # set known params for the return structure
96 shop_id => $self->config->id,
97 shop_description => $self->config->description,
100 number_of_orders => 0,
102 return %fetched_order;
106 my ($self, $shop_part, $todo) = @_;
108 #shop_part is passed as a param
109 croak t8("Need a valid Shop Part for updating Part") unless ref($shop_part) eq 'SL::DB::ShopPart';
110 croak t8("Invalid todo for updating Part") unless $todo =~ m/(price|stock|price_stock|active|all)/;
112 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
113 die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
116 # if the part is connected to a category at all
117 if ($shop_part->shop_category) {
118 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
119 my $temp = { ( id => @{$row_cat}[0] ) };
120 push ( @cat, $temp );
124 my $tax_n_price = $shop_part->get_tax_and_price;
125 my $price = $tax_n_price->{price};
126 my $taxrate = $tax_n_price->{tax};
128 # simple calc for both cases, always give sw6 the calculated gross price
130 if ($self->config->pricetype eq 'brutto') {
132 $net = $price / (1 + $taxrate/100);
133 } elsif ($self->config->pricetype eq 'netto') {
135 $gross = $price * (1 + $taxrate/100);
136 } else { die "Invalid state for price type"; }
139 $update_p->{productNumber} = $part->partnumber;
140 $update_p->{name} = $part->description;
142 $update_p->{stock} = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
143 # JSON::true JSON::false
144 # These special values become JSON true and JSON false values, respectively.
145 # You can also use \1 and \0 directly if you want
146 $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
148 # 1. check if there is already a product
149 my $product_filter = {
152 'value' => $part->partnumber,
154 'field' => 'productNumber'
158 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
159 my $response_code = $ret->responseCode();
160 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
162 my $one_d; # maybe empty
164 $one_d = from_json($ret->responseContent())->{data}->[0];
165 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
166 # edit or create if not found
169 # we need price object structure and taxId
170 $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
171 if ($todo =~ m/(price|all)/) {
172 $update_p->{price}->[0]->{gross} = $gross;
174 undef $update_p->{partNumber}; # we dont need this one
175 $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
176 die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
179 # 1. get the correct tax for this product
189 $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
190 die "Search for Tax with rate: " . $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
192 $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
193 } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent(); };
195 # 2. get the correct currency for this product
196 my $currency_filter = {
199 'value' => SL::DB::Default->get_default_currency,
205 $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
206 die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
207 . $ret->responseContent() unless (200 == $ret->responseCode());
210 $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
211 } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent(); };
213 # 3. add net and gross price and allow variants
214 $update_p->{price}->[0]->{gross} = $gross;
215 $update_p->{price}->[0]->{net} = $net;
216 $update_p->{price}->[0]->{linked} = \1; # link product variants
218 $ret = $self->connector->POST('api/product', to_json($update_p));
219 die "Create for Product " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
222 # if there are images try to sync this with the shop_part
224 $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
225 } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
227 return 1; # no invalid response code -> success
230 sub sync_all_images {
231 my ($self, %params) = @_;
233 $params{set_cover} //= 1;
234 $params{delete_orphaned} //= 0;
236 my $shop_part = delete $params{shop_part};
237 croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
239 my $partnumber = $shop_part->part->partnumber;
240 die "Shop Part but no kivi Partnumber" unless $partnumber;
242 my @upload_img = $shop_part->get_images(want_binary => 1);
244 return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
246 my ($ret, $response_code);
247 # 1. get part uuid and get media associations
248 # 2. create or update the media entry for the filename
249 # 2.1 if no media entry exists create one
251 # 2.2 create or update media_product and set position
252 # 3. optional set cover image
253 # 4. optional delete images in shopware which are not in kivi
255 # 1 get mediaid uuid for prodcut
256 my $product_filter = {
262 'value' => $partnumber,
264 'field' => 'productNumber'
269 $ret = $self->connector->POST('api/search/product', to_json($product_filter));
270 $response_code = $ret->responseCode();
271 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
272 my ($product_id, $media_data);
274 $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
275 # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
276 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
278 # 2 iterate all kivi images and save distinct name for later sync
280 foreach my $img (@upload_img) {
281 die $::locale->text("Need a image title") unless $img->{description};
282 my $distinct_media_name = $partnumber . '_' . $img->{description};
283 $existing_images{$distinct_media_name} = 1;
284 my $image_filter = { 'filter' => [
286 'value' => $distinct_media_name,
288 'field' => 'fileName'
292 $ret = $self->connector->POST('api/search/media', to_json($image_filter));
293 $response_code = $ret->responseCode();
294 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
295 my $current_image_id; # maybe empty
297 $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
298 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
300 # 2.1 no image with this title, create metadata for media and upload image
301 if (!$current_image_id) {
302 # not yet uploaded, create media entry
303 $ret = $self->connector->POST("/api/media?_response=true");
304 $response_code = $ret->responseCode();
305 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
307 $current_image_id = from_json($ret->responseContent())->{data}{id};
308 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
310 # 2.2 update the image data (current_image_id was found or created)
311 $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
314 "Content-Type" => "image/$img->{extension}",
316 $response_code = $ret->responseCode();
317 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
319 # 2.3 check if a product media entry exists for this id
320 my $product_media_filter = {
323 'value' => $product_id,
325 'field' => 'productId'
328 'value' => $current_image_id,
334 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
335 $response_code = $ret->responseCode();
336 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
337 my ($has_product_media, $product_media_id);
339 $has_product_media = from_json($ret->responseContent())->{total};
340 $product_media_id = from_json($ret->responseContent())->{data}->[0]->{id};
341 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
343 # 2.4 ... and either update or create the entry
344 # set shopware position to kivi position
346 $product_media->{position} = $img->{position}; # position may change
348 if ($has_product_media == 0) {
349 # 2.4.1 new entry. link product to media
350 $product_media->{productId} = $product_id;
351 $product_media->{mediaId} = $current_image_id;
352 $ret = $self->connector->POST('api/product-media', to_json($product_media));
353 } elsif ($has_product_media == 1 && $product_media_id) {
354 $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
356 die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
358 $response_code = $ret->responseCode();
359 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
361 # 3. optional set image with position 1 as cover image
362 if ($params{set_cover}) {
363 # set cover if position == 1
364 my $product_media_filter = {
367 'value' => $product_id,
369 'field' => 'productId'
374 'field' => 'position'
379 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
380 $response_code = $ret->responseCode();
381 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
384 $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
385 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
386 $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
387 $response_code = $ret->responseCode();
388 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
390 # 4. optional delete orphaned images in shopware
391 if ($params{delete_orphaned}) {
392 # delete orphaned images
393 my $product_media_filter = {
396 'value' => $product_id,
398 'field' => 'productId'
400 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
401 $response_code = $ret->responseCode();
402 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
405 $img_ary = from_json($ret->responseContent())->{data};
406 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
408 if (scalar @{ $img_ary} > 0) { # maybe no images at all
410 $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
412 while (my ($name, $id) = each %existing_img) {
413 next if $existing_images{$name};
414 $ret = $self->connector->DELETE("api/media/$id");
415 $response_code = $ret->responseCode();
416 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
426 my $ret = $self->connector->POST('api/search/category');
427 my $response_code = $ret->responseCode();
429 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
433 $import = decode_json $ret->responseContent();
435 die "Malformed JSON Data: $_ " . $ret->responseContent();
438 my @daten = @{ $import->{data} };
439 my %categories = map { ($_->{id} => $_) } @daten;
443 my $parent = $categories{$_->{parentId}};
445 $parent->{children} ||= [];
446 push @{ $parent->{children} }, $_;
448 push @categories_tree, $_;
451 return \@categories_tree;
455 my ($self, $ordnumber) = @_;
457 croak t8("No Order Number") unless $ordnumber;
458 # set known params for the return structure
459 my %fetched_order = $self->get_fetched_order_structure;
460 my $assoc = $self->all_open_orders();
462 # overwrite filter for exactly one ordnumber
463 $assoc->{filter}->[0]->{value} = $ordnumber;
464 $assoc->{filter}->[0]->{type} = 'equals';
465 $assoc->{filter}->[0]->{field} = 'orderNumber';
467 # 1. fetch the order and import it as a kivi order
468 # 2. return the number of processed order (1)
469 my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
471 # 1. check for bad request or connection problems
472 if ($one_order->responseCode() != 200) {
473 $fetched_order{error} = 1;
474 $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
475 return \%fetched_order;
478 # 1.1 parse json or exit
481 $content = from_json($one_order->responseContent());
483 $fetched_order{error} = 1;
484 $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
485 return \%fetched_order;
488 # 2. check if we found ONE order at all
489 my $total = $content->{total};
491 $fetched_order{number_of_orders} = 0;
492 return \%fetched_order;
493 } elsif ($total != 1) {
494 $fetched_order{error} = 1;
495 $fetched_order{message} = "More than one Order returned. Invalid State: $total";
496 return \%fetched_order;
499 # 3. there is one valid order, try to import this one
500 if ($self->import_data_to_shop_order($content->{data}->[0])) {
501 %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
503 $fetched_order{message} = "Error: $@";
504 $fetched_order{error} = 1;
506 return \%fetched_order;
512 my %fetched_order = $self->get_fetched_order_structure;
513 my $assoc = $self->all_open_orders();
515 # 1. fetch all open orders and try to import it as a kivi order
516 # 2. return the number of processed order $total
517 my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
519 # 1. check for bad request or connection problems
520 if ($open_orders->responseCode() != 200) {
521 $fetched_order{error} = 1;
522 $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
523 return \%fetched_order;
526 # 1.1 parse json or exit
529 $content = from_json($open_orders->responseContent());
531 $fetched_order{error} = 1;
532 $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
533 return \%fetched_order;
536 # 2. check if we found one or more order at all
537 my $total = $content->{total};
539 $fetched_order{number_of_orders} = 0;
540 return \%fetched_order;
541 } elsif (!$total || !($total > 0)) {
542 $fetched_order{error} = 1;
543 $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
544 return \%fetched_order;
547 # 3. there are open orders. try to import one by one
548 $fetched_order{number_of_orders} = 0;
549 foreach my $open_order (@{ $content->{data} }) {
550 if ($self->import_data_to_shop_order($open_order)) {
551 $fetched_order{number_of_orders}++;
553 $fetched_order{message} .= "Error at importing order with running number:"
554 . $fetched_order{number_of_orders}+1 . ": $@ \n";
555 $fetched_order{error} = 1;
558 return \%fetched_order;
562 my ($self, $partnumber) = @_;
564 $partnumber = $::form->escape($partnumber);
565 my $product_filter = {
568 'value' => $partnumber,
570 'field' => 'productNumber'
574 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
576 my $response_code = $ret->responseCode();
577 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
581 $data_json = decode_json $ret->responseContent();
583 die "Malformed JSON Data: $_ " . $ret->responseContent();
586 # maybe no product was found ...
587 return undef unless scalar @{ $data_json->{data} } > 0;
588 # caller wants this structure:
589 # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
590 # $active_online = $shop_article->{data}->{active};
592 $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
593 $data->{data}->{active} = $data_json->{data}->[0]->{active};
600 my $return = {}; # return for caller
601 my $ret = {}; # internal return
603 # 1. check if we can connect at all
604 # 2. request version number
606 $ret = $self->connector;
607 if (200 != $ret->responseCode()) {
608 $return->{success} = 0;
609 $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
613 $ret = $self->connector->GET('api/_info/version');
614 if (200 == $ret->responseCode()) {
615 my $version = from_json($self->connector->responseContent())->{version};
616 $return->{success} = 1;
617 $return->{data}->{version} = $version;
619 $return->{success} = 0;
620 $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
626 sub set_orderstatus {
627 my ($self, $order_id, $transition) = @_;
629 croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
630 croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
632 $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
633 my $response_code = $ret->responseCode();
634 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
641 my $client = REST::Client->new(host => $self->config->server);
642 $client->addHeader('Content-Type', 'application/json');
643 $client->addHeader('charset', 'UTF-8');
644 $client->addHeader('Accept', 'application/json');
647 client_id => $self->config->login,
648 client_secret => $self->config->password,
649 grant_type => "client_credentials",
652 my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
654 unless (200 == $ret->responseCode()) {
655 $self->{errors} .= $ret->responseContent();
659 my $token = from_json($client->responseContent())->{access_token};
661 $self->{errors} .= "No Auth-Token received";
664 # persist refresh token
665 $client->addHeader('Authorization' => 'Bearer ' . $token);
669 sub import_data_to_shop_order {
670 my ($self, $import) = @_;
672 # failsafe checks for not yet implemented
673 die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
675 # no mapping unless we also have at least one shop order item ...
676 my $order_pos = delete $import->{lineItems};
677 croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
679 my $shop_order = $self->map_data_to_shoporder($import);
681 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
683 my $id = $shop_order->id;
685 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
687 my $active_price_source = $self->config->price_source;
689 foreach my $pos (@positions) {
691 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
692 my %pos_columns = ( description => $pos->{product}->{description},
693 partnumber => $pos->{label},
695 quantity => $pos->{quantity},
696 position => $position,
697 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
698 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
699 shop_order_id => $id,
700 active_price_source => $active_price_source,
702 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
705 $shop_order->positions($position);
707 if ( $self->config->shipping_costs_parts_id ) {
708 die t8("Not yet implemented");
709 # TODO NOT YET Implemented nor tested, this is shopware5 code:
710 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
711 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
712 partnumber => $shipping_part->partnumber,
713 price => $import->{data}->{invoiceShipping},
715 position => $position,
717 shop_order_id => $id,
719 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
720 $shipping_pos_insert->save;
723 my $customer = $shop_order->get_customer;
725 if (ref $customer eq 'SL::DB::Customer') {
726 $shop_order->kivi_customer_id($customer->id);
730 # update state in shopware before transaction ends
731 $self->set_orderstatus($shop_order->shop_trans_id, "process");
735 }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
736 $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
739 sub map_data_to_shoporder {
740 my ($self, $import) = @_;
742 croak "Expect a hash with one order." unless ref $import eq 'HASH';
743 # we need one number and a order date, some total prices and one customer
744 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
745 && $import->{orderDateTime}
746 && ref $import->{price} eq 'HASH'
747 && ref $import->{orderCustomer} eq 'HASH';
749 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
750 die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
752 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
753 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
754 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
756 die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
757 $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
758 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
760 my $billing = $billing_ary->[0];
761 my $shipto = $shipto_ary->[0];
762 # TODO payment info is not used at all
763 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
765 # check mandatory fields from shopware
766 die t8("No billing city") unless $billing->{city};
767 die t8("No shipto city") unless $shipto->{city};
768 die t8("No customer email") unless $import->{orderCustomer}->{email};
771 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
773 time_zone => 'local' );
776 $orderdate = $parser->parse_datetime($import->{orderDateTime});
777 } catch { die "Cannot parse Order Date" . $_ };
779 my $shop_id = $self->config->id;
780 my $tax_included = $self->config->pricetype;
782 # TODO copied from shopware5 connector
783 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
784 my %payment_ids_methods = (
785 # shopware_paymentId => kivitendo_payment_id
787 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
788 my $default_payment_id = $default_payment ? $default_payment->id : undef;
793 amount => $import->{amountTotal},
794 billing_city => $billing->{city},
795 billing_company => $billing->{company},
796 billing_country => $billing->{country}->{name},
797 billing_department => $billing->{department},
798 billing_email => $import->{orderCustomer}->{email},
799 billing_fax => $billing->{fax},
800 billing_firstname => $billing->{firstName},
801 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
802 billing_lastname => $billing->{lastName},
803 billing_phone => $billing->{phone},
804 billing_street => $billing->{street},
805 billing_vat => $billing->{vatId},
806 billing_zipcode => $billing->{zipcode},
807 customer_city => $billing->{city},
808 customer_company => $billing->{company},
809 customer_country => $billing->{country}->{name},
810 customer_department => $billing->{department},
811 customer_email => $billing->{email},
812 customer_fax => $billing->{fax},
813 customer_firstname => $billing->{firstName},
814 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
815 customer_lastname => $billing->{lastName},
816 customer_phone => $billing->{phoneNumber},
817 customer_street => $billing->{street},
818 customer_vat => $billing->{vatId},
819 customer_zipcode => $billing->{zipcode},
820 # customer_newsletter => $customer}->{newsletter},
821 delivery_city => $shipto->{city},
822 delivery_company => $shipto->{company},
823 delivery_country => $shipto->{country}->{name},
824 delivery_department => $shipto->{department},
825 delivery_email => "",
826 delivery_fax => $shipto->{fax},
827 delivery_firstname => $shipto->{firstName},
828 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
829 delivery_lastname => $shipto->{lastName},
830 delivery_phone => $shipto->{phone},
831 delivery_street => $shipto->{street},
832 delivery_vat => $shipto->{vatId},
833 delivery_zipcode => $shipto->{zipCode},
834 # host => $shop}->{hosts},
835 netamount => $import->{amountNet},
836 order_date => $orderdate,
837 payment_description => $payment->{name},
838 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
839 tax_included => $tax_included eq "brutto" ? 1 : 0,
840 shop_ordernumber => $import->{orderNumber},
842 shop_trans_id => $import->{id},
844 #remote_ip => $import->{remoteAddress},
845 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
846 #sepa_bic => $import->{paymentIntances}->{bic},
847 #sepa_iban => $import->{paymentIntances}->{iban},
848 #shipping_costs => $import->{invoiceShipping},
849 #shipping_costs_net => $import->{invoiceShippingNet},
850 #shop_c_billing_id => $import->{billing}->{customerId},
851 #shop_c_billing_number => $import->{billing}->{number},
852 #shop_c_delivery_id => $import->{shipping}->{id},
853 #shop_customer_id => $import->{customerId},
854 #shop_customer_number => $import->{billing}->{number},
855 #shop_customer_comment => $import->{customerComment},
858 my $shop_order = SL::DB::ShopOrder->new(%columns);
864 return encode('UTF-8', $value // '');
875 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
882 =head1 AVAILABLE METHODS
886 =item C<get_one_order>
888 =item C<get_new_orders>
892 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
894 The connecting key for shopware to kivi images is the image name.
895 To get distinct entries the kivi partnumber is combined with the title (description)
896 of the image. Therefore part1000_someTitlefromUser should be unique in
898 All image data is simply send to shopware whether or not image data
899 has been edited recently.
900 If set_cover is set, the image with the position 1 will be used as
901 the shopware cover image.
902 If delete_orphaned ist set, all images related to the shopware product
903 which are not also in kivitendo will be deleted.
904 Shopware (6.4.x) takes care of deleting all the relations if the media
905 entry for the image is deleted.
906 More on media and Shopware6 can be found here:
907 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
912 =item C<get_categories>
916 Tries to establish a connection and in a second step
917 tries to get the server's version number.
918 Returns a hashref with the data structure the Base class expects.
920 =item C<set_orderstatus>
922 =item C<init_connector>
924 Inits the connection to the REST Server.
925 Errors are collected in $self->{errors} and undef will be returned.
926 If successful returns a REST::Client object for further communications.
932 L<SL::ShopConnector::ALL>
942 =item * Map all data to shop_order
944 Missing fields are commented in the sub map_data_to_shoporder.
945 Some items are SEPA debit info, IP adress, delivery costs etc
946 Furthermore Shopware6 uses currency, country and locales information.
949 #customer_newsletter => $customer}->{newsletter},
950 #remote_ip => $import->{remoteAddress},
951 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
952 #sepa_bic => $import->{paymentIntances}->{bic},
953 #sepa_iban => $import->{paymentIntances}->{iban},
954 #shipping_costs => $import->{invoiceShipping},
955 #shipping_costs_net => $import->{invoiceShippingNet},
956 #shop_c_billing_id => $import->{billing}->{customerId},
957 #shop_c_billing_number => $import->{billing}->{number},
958 #shop_c_delivery_id => $import->{shipping}->{id},
959 #shop_customer_id => $import->{customerId},
960 #shop_customer_number => $import->{billing}->{number},
961 #shop_customer_comment => $import->{customerComment},
963 =item * Use shipping_costs_parts_id for additional shipping costs
965 Currently dies if a shipping_costs_parts_id is set in the config
967 =item * Payment Infos can be read from shopware but is not linked with kivi
969 Unused data structures in sub map_data_to_shoporder => payment_ary
971 =item * Delete orphaned images is new in this connector, but should be in a separate method
973 =item * Fetch from last order number is ignored and should not be needed
975 Fetch orders also sets the state of the order from open to process. The state setting
976 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
977 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
978 and ignores any shopware order transition state.
980 =item * Get one order and get new orders is basically the same except for the filter
982 Right now the returning structure and the common parts of the filter are in two separate functions
986 Many error messages are thrown, but at least the more common cases should be localized.
992 Jan Büren jan@kivitendo.de