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();
585 # maybe no product was found ...
586 return undef unless scalar @{ $data_json->{data} } > 0;
587 # caller wants this structure:
588 # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
589 # $active_online = $shop_article->{data}->{active};
591 $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
592 $data->{data}->{active} = $data_json->{data}->[0]->{active};
599 my $return = {}; # return for caller
600 my $ret = {}; # internal return
602 # 1. check if we can connect at all
603 # 2. request version number
605 $ret = $self->connector;
606 if (200 != $ret->responseCode()) {
607 $return->{success} = 0;
608 $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
612 $ret = $self->connector->GET('api/_info/version');
613 if (200 == $ret->responseCode()) {
614 my $version = from_json($self->connector->responseContent())->{version};
615 $return->{success} = 1;
616 $return->{data}->{version} = $version;
618 $return->{success} = 0;
619 $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
625 sub set_orderstatus {
626 my ($self, $order_id, $transition) = @_;
628 croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
629 croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
631 $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
632 my $response_code = $ret->responseCode();
633 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
640 my $client = REST::Client->new(host => $self->config->server);
641 $client->addHeader('Content-Type', 'application/json');
642 $client->addHeader('charset', 'UTF-8');
643 $client->addHeader('Accept', 'application/json');
646 client_id => $self->config->login,
647 client_secret => $self->config->password,
648 grant_type => "client_credentials",
651 my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
653 unless (200 == $ret->responseCode()) {
654 $self->{errors} .= $ret->responseContent();
658 my $token = from_json($client->responseContent())->{access_token};
660 $self->{errors} .= "No Auth-Token received";
663 # persist refresh token
664 $client->addHeader('Authorization' => 'Bearer ' . $token);
668 sub import_data_to_shop_order {
669 my ($self, $import) = @_;
671 # failsafe checks for not yet implemented
672 die $::locale->text('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
674 # no mapping unless we also have at least one shop order item ...
675 my $order_pos = delete $import->{lineItems};
676 croak("No Order items fetched") unless ref $order_pos eq 'ARRAY';
678 my $shop_order = $self->map_data_to_shoporder($import);
680 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
682 my $id = $shop_order->id;
684 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
686 my $active_price_source = $self->config->price_source;
688 foreach my $pos (@positions) {
690 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
691 my %pos_columns = ( description => $pos->{product}->{description},
692 partnumber => $pos->{label},
694 quantity => $pos->{quantity},
695 position => $position,
696 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
697 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
698 shop_order_id => $id,
699 active_price_source => $active_price_source,
701 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
704 $shop_order->positions($position);
706 if ( $self->config->shipping_costs_parts_id ) {
707 die "Not yet implemented";
708 # TODO NOT YET Implemented nor tested, this is shopware5 code:
709 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
710 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
711 partnumber => $shipping_part->partnumber,
712 price => $import->{data}->{invoiceShipping},
714 position => $position,
716 shop_order_id => $id,
718 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
719 $shipping_pos_insert->save;
722 my $customer = $shop_order->get_customer;
724 if (ref $customer eq 'SL::DB::Customer') {
725 $shop_order->kivi_customer_id($customer->id);
729 # update state in shopware before transaction ends
730 $self->set_orderstatus($shop_order->shop_trans_id, "process");
734 }) || die ('error while saving shop order ' . $shop_order->{shop_ordernumber} . 'Error: ' . $shop_order->db->error . "\n" .
735 'generic exception:' . $@);
738 sub map_data_to_shoporder {
739 my ($self, $import) = @_;
741 croak "Expect a hash with one order." unless ref $import eq 'HASH';
742 # we need one number and a order date, some total prices and one customer
743 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
744 && $import->{orderDateTime}
745 && ref $import->{price} eq 'HASH'
746 && ref $import->{orderCustomer} eq 'HASH';
748 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
749 die "Cannot get shippingOrderAddressId for $import->{orderNumber}" unless $shipto_id;
751 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
752 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
753 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
755 croak("No Billing and ship to address, for Order Number " . $import->{orderNumber} .
756 "ID Billing:" . $import->{billingAddressId} . " ID Shipping $shipto_id ")
757 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
759 my $billing = $billing_ary->[0];
760 my $shipto = $shipto_ary->[0];
761 # TODO payment info is not used at all
762 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
764 croak "No billing city" unless $billing->{city};
765 croak "No shipto city" unless $shipto->{city};
766 croak "No customer email" unless $import->{orderCustomer}->{email};
769 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
771 time_zone => 'local' );
774 $orderdate = $parser->parse_datetime($import->{orderDateTime});
775 } catch { die "Cannot parse Order Date" . $_ };
777 my $shop_id = $self->config->id;
778 my $tax_included = $self->config->pricetype;
780 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
781 my %payment_ids_methods = (
782 # shopware_paymentId => kivitendo_payment_id
784 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
785 my $default_payment_id = $default_payment ? $default_payment->id : undef;
790 amount => $import->{amountTotal},
791 billing_city => $billing->{city},
792 billing_company => $billing->{company},
793 billing_country => $billing->{country}->{name},
794 billing_department => $billing->{department},
795 billing_email => $import->{orderCustomer}->{email},
796 billing_fax => $billing->{fax},
797 billing_firstname => $billing->{firstName},
798 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
799 billing_lastname => $billing->{lastName},
800 billing_phone => $billing->{phone},
801 billing_street => $billing->{street},
802 billing_vat => $billing->{vatId},
803 billing_zipcode => $billing->{zipcode},
804 customer_city => $billing->{city},
805 customer_company => $billing->{company},
806 customer_country => $billing->{country}->{name},
807 customer_department => $billing->{department},
808 customer_email => $billing->{email},
809 customer_fax => $billing->{fax},
810 customer_firstname => $billing->{firstName},
811 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
812 customer_lastname => $billing->{lastName},
813 customer_phone => $billing->{phoneNumber},
814 customer_street => $billing->{street},
815 customer_vat => $billing->{vatId},
816 customer_zipcode => $billing->{zipcode},
817 # customer_newsletter => $customer}->{newsletter},
818 delivery_city => $shipto->{city},
819 delivery_company => $shipto->{company},
820 delivery_country => $shipto->{country}->{name},
821 delivery_department => $shipto->{department},
822 delivery_email => "",
823 delivery_fax => $shipto->{fax},
824 delivery_firstname => $shipto->{firstName},
825 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
826 delivery_lastname => $shipto->{lastName},
827 delivery_phone => $shipto->{phone},
828 delivery_street => $shipto->{street},
829 delivery_vat => $shipto->{vatId},
830 delivery_zipcode => $shipto->{zipCode},
831 # host => $shop}->{hosts},
832 netamount => $import->{amountNet},
833 order_date => $orderdate,
834 payment_description => $payment->{name},
835 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
836 tax_included => $tax_included eq "brutto" ? 1 : 0,
837 shop_ordernumber => $import->{orderNumber},
839 shop_trans_id => $import->{id},
841 #remote_ip => $import->{remoteAddress},
842 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
843 #sepa_bic => $import->{paymentIntances}->{bic},
844 #sepa_iban => $import->{paymentIntances}->{iban},
845 #shipping_costs => $import->{invoiceShipping},
846 #shipping_costs_net => $import->{invoiceShippingNet},
847 #shop_c_billing_id => $import->{billing}->{customerId},
848 #shop_c_billing_number => $import->{billing}->{number},
849 #shop_c_delivery_id => $import->{shipping}->{id},
850 #shop_customer_id => $import->{customerId},
851 #shop_customer_number => $import->{billing}->{number},
852 #shop_customer_comment => $import->{customerComment},
855 my $shop_order = SL::DB::ShopOrder->new(%columns);
861 return encode('UTF-8', $value // '');
872 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
879 =head1 AVAILABLE METHODS
883 =item C<get_one_order>
885 =item C<get_new_orders>
889 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
891 The important key for shopware is the image name. To get distinct
892 entries the kivi partnumber is combined with the title (description)
893 of the image. Therefore part1000_someTitlefromUser should be unique in
895 All image data is simply send to shopware whether or not image data
896 has been edited recently.
897 If set_cover is set, the image with the position 1 will be used as
898 the shopware cover image.
899 If delete_orphaned ist set, all images related to the shopware product
900 which are not also in kivitendo will be deleted.
901 Shopware (6.4.x) takes care of deleting all the relations if the media
902 entry for the image is deleted.
903 More on media and Shopware6 can be found here:
904 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
909 =item C<get_categories>
913 Tries to establish a connection and in a second step
914 tries to get the server's version number.
915 Returns a hashref with the data structure the Base class expects.
917 =item C<set_orderstatus>
919 =item C<init_connector>
921 Inits the connection to the REST Server.
922 Errors are collected in $self->{errors} and undef will be returned.
923 If successful returns a REST::Client object for further communications.
929 L<SL::ShopConnector::ALL>
939 =item * Map all data to shop_order
941 Missing fields are commented in the sub map_data_to_shoporder.
942 Some items are SEPA debit info, IP adress, delivery costs etc
943 Furthermore Shopware6 uses currency, country and locales information.
945 =item * Use shipping_costs_parts_id for additional shipping costs
947 Currently dies if a shipping_costs_parts_id is set in the config
949 =item * Payment Infos can be read from shopware but is not linked with kivi
951 Unused data structures in sub map_data_to_shoporder => payment_ary
953 =item * Delete orphaned images is new in this connector, but should be in a separate method
955 =item * Fetch from last order number is ignored and should not be needed
957 Fetch orders also sets the state of the order from open to process. The state setting
958 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
959 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
960 and ignores any shopware order transition state.
962 =item * Get one order and get new orders is basically the same except for the filter
964 Right now the returning structure and the common parts of the filter are in two separate functions
968 Many error messages are thrown, but at least the more common cases should be localized.
974 Jan Büren jan@kivitendo.de