1 package SL::ShopConnector::Shopware6;
5 use parent qw(SL::ShopConnector::Base);
13 use SL::Helper::Flash;
15 use Rose::Object::MakeMethods::Generic (
16 'scalar --get_set_init' => [ qw(connector) ],
26 'shippingMethod' => [],
27 'shippingOrderAddress' => {
37 'orderCustomer' => [],
56 'documents' => { # currently not used
68 'limit' => $self->config->orders_to_fetch ? $self->config->orders_to_fetch : undef,
72 'field' => 'billingAddressId',
73 'definition' => 'order_address',
74 'name' => 'BillingAddress',
80 'value' => 'open', # open or completed (mind the past)
82 'field' => 'order.stateMachineState.technicalName'
85 'total-count-mode' => 0
90 # used for get_new_orders and get_one_order
91 sub get_fetched_order_structure {
93 # set known params for the return structure
95 shop_id => $self->config->id,
96 shop_description => $self->config->description,
99 number_of_orders => 0,
101 return %fetched_order;
105 my ($self, $shop_part, $todo) = @_;
107 #shop_part is passed as a param
108 croak "Need a valid Shop Part for updating Part" unless ref($shop_part) eq 'SL::DB::ShopPart';
109 croak "Invalid todo for updating Part" unless $todo =~ m/(price|stock|price_stock|active|all)/;
111 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
112 die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
115 # if the part is connected to a category at all
116 if ($shop_part->shop_category) {
117 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
118 my $temp = { ( id => @{$row_cat}[0] ) };
119 push ( @cat, $temp );
123 my $tax_n_price = $shop_part->get_tax_and_price;
124 my $price = $tax_n_price->{price};
125 my $taxrate = $tax_n_price->{tax};
127 # simple calc for both cases, always give sw6 the calculated gross price
129 if ($self->config->pricetype eq 'brutto') {
131 $net = $price / (1 + $taxrate/100);
132 } elsif ($self->config->pricetype eq 'netto') {
134 $gross = $price * (1 + $taxrate/100);
135 } else { die "Invalid state for price type"; }
138 $update_p->{productNumber} = $part->partnumber;
139 $update_p->{name} = $part->description;
141 $update_p->{stock} = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
142 # JSON::true JSON::false
143 # These special values become JSON true and JSON false values, respectively.
144 # You can also use \1 and \0 directly if you want
145 $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
147 # 1. check if there is already a product
148 my $product_filter = {
151 'value' => $part->partnumber,
153 'field' => 'productNumber'
157 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
158 my $response_code = $ret->responseCode();
159 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
161 my $one_d; # maybe empty
163 $one_d = from_json($ret->responseContent())->{data}->[0];
164 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
165 # edit or create if not found
168 # we need price object structure and taxId
169 $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
170 if ($todo =~ m/(price|all)/) {
171 $update_p->{price}->[0]->{gross} = $gross;
173 undef $update_p->{partNumber}; # we dont need this one
174 $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
175 die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
178 # 1. get the correct tax for this product
188 $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
189 die "Search for Tax with rate: " . $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
191 $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
192 } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent(); };
194 # 2. get the correct currency for this product
195 my $currency_filter = {
198 'value' => SL::DB::Default->get_default_currency,
204 $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
205 die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
206 . $ret->responseContent() unless (200 == $ret->responseCode());
209 $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
210 } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent(); };
212 # 3. add net and gross price and allow variants
213 $update_p->{price}->[0]->{gross} = $gross;
214 $update_p->{price}->[0]->{net} = $net;
215 $update_p->{price}->[0]->{linked} = \1; # link product variants
217 $ret = $self->connector->POST('api/product', to_json($update_p));
218 die "Create for Product " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
221 # if there are images try to sync this with the shop_part
223 $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
224 } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
226 return 1; # no invalid response code -> success
229 sub sync_all_images {
230 my ($self, %params) = @_;
232 $params{set_cover} //= 1;
233 $params{delete_orphaned} //= 0;
235 my $shop_part = delete $params{shop_part};
236 croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
238 my $partnumber = $shop_part->part->partnumber;
239 die "Shop Part but no kivi Partnumber" unless $partnumber;
241 my @upload_img = $shop_part->get_images(want_binary => 1);
243 return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
245 my ($ret, $response_code);
246 # 1. get part uuid and get media associations
247 # 2. create or update the media entry for the filename
248 # 2.1 if no media entry exists create one
250 # 2.2 create or update media_product and set position
251 # 3. optional set cover image
252 # 4. optional delete images in shopware which are not in kivi
254 # 1 get mediaid uuid for prodcut
255 my $product_filter = {
261 'value' => $partnumber,
263 'field' => 'productNumber'
268 $ret = $self->connector->POST('api/search/product', to_json($product_filter));
269 $response_code = $ret->responseCode();
270 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
271 my ($product_id, $media_data);
273 $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
274 # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
275 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
277 # 2 iterate all kivi images and save distinct name for later sync
279 foreach my $img (@upload_img) {
280 die $::locale->text("Need a image title") unless $img->{description};
281 my $distinct_media_name = $partnumber . '_' . $img->{description};
282 $existing_images{$distinct_media_name} = 1;
283 my $image_filter = { 'filter' => [
285 'value' => $distinct_media_name,
287 'field' => 'fileName'
291 $ret = $self->connector->POST('api/search/media', to_json($image_filter));
292 $response_code = $ret->responseCode();
293 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
294 my $current_image_id; # maybe empty
296 $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
297 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
299 # 2.1 no image with this title, create metadata for media and upload image
300 if (!$current_image_id) {
301 # not yet uploaded, create media entry
302 $ret = $self->connector->POST("/api/media?_response=true");
303 $response_code = $ret->responseCode();
304 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
306 $current_image_id = from_json($ret->responseContent())->{data}{id};
307 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
309 # 2.2 update the image data (current_image_id was found or created)
310 $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
313 "Content-Type" => "image/$img->{extension}",
315 $response_code = $ret->responseCode();
316 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
318 # 2.3 check if a product media entry exists for this id
319 my $product_media_filter = {
322 'value' => $product_id,
324 'field' => 'productId'
327 'value' => $current_image_id,
333 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
334 $response_code = $ret->responseCode();
335 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
336 my ($has_product_media, $product_media_id);
338 $has_product_media = from_json($ret->responseContent())->{total};
339 $product_media_id = from_json($ret->responseContent())->{data}->[0]->{id};
340 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
342 # 2.4 ... and either update or create the entry
343 # set shopware position to kivi position
345 $product_media->{position} = $img->{position}; # position may change
347 if ($has_product_media == 0) {
348 # 2.4.1 new entry. link product to media
349 $product_media->{productId} = $product_id;
350 $product_media->{mediaId} = $current_image_id;
351 $ret = $self->connector->POST('api/product-media', to_json($product_media));
352 } elsif ($has_product_media == 1 && $product_media_id) {
353 $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
355 die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
357 $response_code = $ret->responseCode();
358 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
360 # 3. optional set image with position 1 as cover image
361 if ($params{set_cover}) {
362 # set cover if position == 1
363 my $product_media_filter = {
366 'value' => $product_id,
368 'field' => 'productId'
373 'field' => 'position'
378 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
379 $response_code = $ret->responseCode();
380 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
383 $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
384 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
385 $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
386 $response_code = $ret->responseCode();
387 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
389 # 4. optional delete orphaned images in shopware
390 if ($params{delete_orphaned}) {
391 # delete orphaned images
392 my $product_media_filter = {
395 'value' => $product_id,
397 'field' => 'productId'
399 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
400 $response_code = $ret->responseCode();
401 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
404 $img_ary = from_json($ret->responseContent())->{data};
405 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
407 if (scalar @{ $img_ary} > 0) { # maybe no images at all
409 $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
411 while (my ($name, $id) = each %existing_img) {
412 next if $existing_images{$name};
413 $ret = $self->connector->DELETE("api/media/$id");
414 $response_code = $ret->responseCode();
415 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
425 my $ret = $self->connector->POST('api/search/category');
426 my $response_code = $ret->responseCode();
428 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
432 $import = decode_json $ret->responseContent();
434 die "Malformed JSON Data: $_ " . $ret->responseContent();
437 my @daten = @{ $import->{data} };
438 my %categories = map { ($_->{id} => $_) } @daten;
442 my $parent = $categories{$_->{parentId}};
444 $parent->{children} ||= [];
445 push @{ $parent->{children} }, $_;
447 push @categories_tree, $_;
450 return \@categories_tree;
454 my ($self, $ordnumber) = @_;
456 die "No ordnumber" unless $ordnumber;
457 # set known params for the return structure
458 my %fetched_order = $self->get_fetched_order_structure;
459 my $assoc = $self->all_open_orders();
461 # overwrite filter for exactly one ordnumber
462 $assoc->{filter}->[0]->{value} = $ordnumber;
463 $assoc->{filter}->[0]->{type} = 'equals';
464 $assoc->{filter}->[0]->{field} = 'orderNumber';
466 # 1. fetch the order and import it as a kivi order
467 # 2. return the number of processed order (1)
468 my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
470 # 1. check for bad request or connection problems
471 if ($one_order->responseCode() != 200) {
472 $fetched_order{error} = 1;
473 $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
474 return \%fetched_order;
477 # 1.1 parse json or exit
480 $content = from_json($one_order->responseContent());
482 $fetched_order{error} = 1;
483 $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
484 return \%fetched_order;
487 # 2. check if we found ONE order at all
488 my $total = $content->{total};
490 $fetched_order{number_of_orders} = 0;
491 return \%fetched_order;
492 } elsif ($total != 1) {
493 $fetched_order{error} = 1;
494 $fetched_order{message} = "More than one Order returned. Invalid State: $total";
495 return \%fetched_order;
498 # 3. there is one valid order, try to import this one
499 if ($self->import_data_to_shop_order($content->{data}->[0])) {
500 %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
502 $fetched_order{message} = "Error: $@";
503 $fetched_order{error} = 1;
505 return \%fetched_order;
511 my %fetched_order = $self->get_fetched_order_structure;
512 my $assoc = $self->all_open_orders();
514 # 1. fetch all open orders and try to import it as a kivi order
515 # 2. return the number of processed order $total
516 my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
518 # 1. check for bad request or connection problems
519 if ($open_orders->responseCode() != 200) {
520 $fetched_order{error} = 1;
521 $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
522 return \%fetched_order;
525 # 1.1 parse json or exit
528 $content = from_json($open_orders->responseContent());
530 $fetched_order{error} = 1;
531 $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
532 return \%fetched_order;
535 # 2. check if we found one or more order at all
536 my $total = $content->{total};
538 $fetched_order{number_of_orders} = 0;
539 return \%fetched_order;
540 } elsif (!$total || !($total > 0)) {
541 $fetched_order{error} = 1;
542 $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
543 return \%fetched_order;
546 # 3. there are open orders. try to import one by one
547 $fetched_order{number_of_orders} = 0;
548 foreach my $open_order (@{ $content->{data} }) {
549 if ($self->import_data_to_shop_order($open_order)) {
550 $fetched_order{number_of_orders}++;
552 $fetched_order{message} .= "Error at importing order with running number:"
553 . $fetched_order{number_of_orders}+1 . ": $@ \n";
554 $fetched_order{error} = 1;
557 return \%fetched_order;
561 my ($self, $partnumber) = @_;
563 $partnumber = $::form->escape($partnumber);
564 my $product_filter = {
567 'value' => $partnumber,
569 'field' => 'productNumber'
573 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
575 my $response_code = $ret->responseCode();
576 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
580 $data_json = decode_json $ret->responseContent();
582 die "Malformed JSON Data: $_ " . $ret->responseContent();
584 # caller wants this structure:
585 # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
586 # $active_online = $shop_article->{data}->{active};
588 $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
589 $data->{data}->{active} = $data_json->{data}->[0]->{active};
596 my $return = {}; # return for caller
597 my $ret = {}; # internal return
599 # 1. check if we can connect at all
600 # 2. request version number
602 $ret = $self->connector;
603 if (200 != $ret->responseCode()) {
604 $return->{success} = 0;
605 $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
609 $ret = $self->connector->GET('api/_info/version');
610 if (200 == $ret->responseCode()) {
611 my $version = from_json($self->connector->responseContent())->{version};
612 $return->{success} = 1;
613 $return->{data}->{version} = $version;
615 $return->{success} = 0;
616 $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
622 sub set_orderstatus {
623 my ($self, $order_id, $transition) = @_;
625 croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
626 croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
628 $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
629 my $response_code = $ret->responseCode();
630 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
637 my $client = REST::Client->new(host => $self->config->server);
638 $client->addHeader('Content-Type', 'application/json');
639 $client->addHeader('charset', 'UTF-8');
640 $client->addHeader('Accept', 'application/json');
643 client_id => $self->config->login,
644 client_secret => $self->config->password,
645 grant_type => "client_credentials",
648 my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
650 unless (200 == $ret->responseCode()) {
651 $self->{errors} .= $ret->responseContent();
655 my $token = from_json($client->responseContent())->{access_token};
657 $self->{errors} .= "No Auth-Token received";
660 # persist refresh token
661 $client->addHeader('Authorization' => 'Bearer ' . $token);
665 sub import_data_to_shop_order {
666 my ($self, $import) = @_;
668 # failsafe checks for not yet implemented
669 die $::locale->text('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
671 # no mapping unless we also have at least one shop order item ...
672 my $order_pos = delete $import->{lineItems};
673 croak("No Order items fetched") unless ref $order_pos eq 'ARRAY';
675 my $shop_order = $self->map_data_to_shoporder($import);
677 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
679 my $id = $shop_order->id;
681 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
683 my $active_price_source = $self->config->price_source;
685 foreach my $pos (@positions) {
687 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
688 my %pos_columns = ( description => $pos->{product}->{description},
689 partnumber => $pos->{label},
691 quantity => $pos->{quantity},
692 position => $position,
693 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
694 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
695 shop_order_id => $id,
696 active_price_source => $active_price_source,
698 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
701 $shop_order->positions($position);
703 if ( $self->config->shipping_costs_parts_id ) {
704 die "Not yet implemented";
705 # TODO NOT YET Implemented nor tested, this is shopware5 code:
706 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
707 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
708 partnumber => $shipping_part->partnumber,
709 price => $import->{data}->{invoiceShipping},
711 position => $position,
713 shop_order_id => $id,
715 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
716 $shipping_pos_insert->save;
719 my $customer = $shop_order->get_customer;
721 if (ref $customer eq 'SL::DB::Customer') {
722 $shop_order->kivi_customer_id($customer->id);
726 # update state in shopware before transaction ends
727 $self->set_orderstatus($shop_order->shop_trans_id, "process");
731 }) || die ('error while saving shop order ' . $shop_order->{shop_ordernumber} . 'Error: ' . $shop_order->db->error . "\n" .
732 'generic exception:' . $@);
735 sub map_data_to_shoporder {
736 my ($self, $import) = @_;
738 croak "Expect a hash with one order." unless ref $import eq 'HASH';
739 # we need one number and a order date, some total prices and one customer
740 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
741 && $import->{orderDateTime}
742 && ref $import->{price} eq 'HASH'
743 && ref $import->{orderCustomer} eq 'HASH';
745 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
746 die "Cannot get shippingOrderAddressId for $import->{orderNumber}" unless $shipto_id;
748 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
749 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
750 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
752 croak("No Billing and ship to address, for Order Number " . $import->{orderNumber} .
753 "ID Billing:" . $import->{billingAddressId} . " ID Shipping $shipto_id ")
754 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
756 my $billing = $billing_ary->[0];
757 my $shipto = $shipto_ary->[0];
758 # TODO payment info is not used at all
759 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
761 croak "No billing city" unless $billing->{city};
762 croak "No shipto city" unless $shipto->{city};
763 croak "No customer email" unless $import->{orderCustomer}->{email};
766 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
768 time_zone => 'local' );
771 $orderdate = $parser->parse_datetime($import->{orderDateTime});
772 } catch { die "Cannot parse Order Date" . $_ };
774 my $shop_id = $self->config->id;
775 my $tax_included = $self->config->pricetype;
777 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
778 my %payment_ids_methods = (
779 # shopware_paymentId => kivitendo_payment_id
781 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
782 my $default_payment_id = $default_payment ? $default_payment->id : undef;
787 amount => $import->{amountTotal},
788 billing_city => $billing->{city},
789 billing_company => $billing->{company},
790 billing_country => $billing->{country}->{name},
791 billing_department => $billing->{department},
792 billing_email => $import->{orderCustomer}->{email},
793 billing_fax => $billing->{fax},
794 billing_firstname => $billing->{firstName},
795 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
796 billing_lastname => $billing->{lastName},
797 billing_phone => $billing->{phone},
798 billing_street => $billing->{street},
799 billing_vat => $billing->{vatId},
800 billing_zipcode => $billing->{zipcode},
801 customer_city => $billing->{city},
802 customer_company => $billing->{company},
803 customer_country => $billing->{country}->{name},
804 customer_department => $billing->{department},
805 customer_email => $billing->{email},
806 customer_fax => $billing->{fax},
807 customer_firstname => $billing->{firstName},
808 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
809 customer_lastname => $billing->{lastName},
810 customer_phone => $billing->{phoneNumber},
811 customer_street => $billing->{street},
812 customer_vat => $billing->{vatId},
813 customer_zipcode => $billing->{zipcode},
814 # customer_newsletter => $customer}->{newsletter},
815 delivery_city => $shipto->{city},
816 delivery_company => $shipto->{company},
817 delivery_country => $shipto->{country}->{name},
818 delivery_department => $shipto->{department},
819 delivery_email => "",
820 delivery_fax => $shipto->{fax},
821 delivery_firstname => $shipto->{firstName},
822 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
823 delivery_lastname => $shipto->{lastName},
824 delivery_phone => $shipto->{phone},
825 delivery_street => $shipto->{street},
826 delivery_vat => $shipto->{vatId},
827 delivery_zipcode => $shipto->{zipCode},
828 # host => $shop}->{hosts},
829 netamount => $import->{amountNet},
830 order_date => $orderdate,
831 payment_description => $payment->{name},
832 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
833 tax_included => $tax_included eq "brutto" ? 1 : 0,
834 shop_ordernumber => $import->{orderNumber},
836 shop_trans_id => $import->{id},
838 #remote_ip => $import->{remoteAddress},
839 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
840 #sepa_bic => $import->{paymentIntances}->{bic},
841 #sepa_iban => $import->{paymentIntances}->{iban},
842 #shipping_costs => $import->{invoiceShipping},
843 #shipping_costs_net => $import->{invoiceShippingNet},
844 #shop_c_billing_id => $import->{billing}->{customerId},
845 #shop_c_billing_number => $import->{billing}->{number},
846 #shop_c_delivery_id => $import->{shipping}->{id},
847 #shop_customer_id => $import->{customerId},
848 #shop_customer_number => $import->{billing}->{number},
849 #shop_customer_comment => $import->{customerComment},
852 my $shop_order = SL::DB::ShopOrder->new(%columns);
858 return encode('UTF-8', $value // '');
869 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
876 =head1 AVAILABLE METHODS
880 =item C<get_one_order>
882 =item C<get_new_orders>
886 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
888 The important key for shopware is the image name. To get distinct
889 entries the kivi partnumber is combined with the title (description)
890 of the image. Therefore part1000_someTitlefromUser should be unique in
892 All image data is simply send to shopware whether or not image data
893 has been edited recently.
894 If set_cover is set, the image with the position 1 will be used as
895 the shopware cover image.
896 If delete_orphaned ist set, all images related to the shopware product
897 which are not also in kivitendo will be deleted.
898 Shopware (6.4.x) takes care of deleting all the relations if the media
899 entry for the image is deleted.
900 More on media and Shopware6 can be found here:
901 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
906 =item C<get_categories>
910 Tries to establish a connection and in a second step
911 tries to get the server's version number.
912 Returns a hashref with the data structure the Base class expects.
914 =item C<set_orderstatus>
916 =item C<init_connector>
918 Inits the connection to the REST Server.
919 Errors are collected in $self->{errors} and undef will be returned.
920 If successful returns a REST::Client object for further communications.
926 L<SL::ShopConnector::ALL>
936 =item * Map all data to shop_order
938 Missing fields are commented in the sub map_data_to_shoporder.
939 Some items are SEPA debit info, IP adress, delivery costs etc
940 Furthermore Shopware6 uses currency, country and locales information.
942 =item * Use shipping_costs_parts_id for additional shipping costs
944 Currently dies if a shipping_costs_parts_id is set in the config
946 =item * Payment Infos can be read from shopware but is not linked with kivi
948 Unused data structures in sub map_data_to_shoporder => payment_ary
950 =item * Delete orphaned images is new in this connector, but should be in a separate method
952 =item * Fetch from last order number is ignored and should not be needed
954 Fetch orders also sets the state of the order from open to process. The state setting
955 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
956 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
957 and ignores any shopware order transition state.
959 =item * Get one order and get new orders is basically the same except for the filter
961 Right now the returning structure and the common parts of the filter are in two separate functions
965 Many error messages are thrown, but at least the more common cases should be localized.
971 Jan Büren jan@kivitendo.de