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);
664 # ua multiple form-data
668 my $section = 'bekuplast_elo';
669 my $config = $::lx_office_conf{$section} || {};
671 # mandatory config parameters
672 foreach (qw(user pass apikey)) {
673 die "parameter '$_' in section '$section' must be set in config file" if !defined $config->{$_};
675 $client = REST::Client->new(host => $self->host);
676 # no test case available in ELO API
677 # and set ua we need this, because ELO wants multippart/form-data
678 my $ua = $self->client->getUseragent();
680 $ua->default_header(Authorization => 'Basic ' . encode_base64($config->{user} . ':' . $config->{pass}),
681 apikey => $config->{apikey},
682 'Content-Type' => 'multipart/form-data',
689 sub import_data_to_shop_order {
690 my ($self, $import) = @_;
692 # failsafe checks for not yet implemented
693 die $::locale->text('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
695 # no mapping unless we also have at least one shop order item ...
696 my $order_pos = delete $import->{lineItems};
697 croak("No Order items fetched") unless ref $order_pos eq 'ARRAY';
699 my $shop_order = $self->map_data_to_shoporder($import);
701 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
703 my $id = $shop_order->id;
705 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
707 my $active_price_source = $self->config->price_source;
709 foreach my $pos (@positions) {
711 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
712 my %pos_columns = ( description => $pos->{product}->{description},
713 partnumber => $pos->{label},
715 quantity => $pos->{quantity},
716 position => $position,
717 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
718 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
719 shop_order_id => $id,
720 active_price_source => $active_price_source,
722 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
725 $shop_order->positions($position);
727 if ( $self->config->shipping_costs_parts_id ) {
728 die "Not yet implemented";
729 # TODO NOT YET Implemented nor tested, this is shopware5 code:
730 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
731 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
732 partnumber => $shipping_part->partnumber,
733 price => $import->{data}->{invoiceShipping},
735 position => $position,
737 shop_order_id => $id,
739 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
740 $shipping_pos_insert->save;
743 my $customer = $shop_order->get_customer;
745 if (ref $customer eq 'SL::DB::Customer') {
746 $shop_order->kivi_customer_id($customer->id);
750 # update state in shopware before transaction ends
751 $self->set_orderstatus($shop_order->shop_trans_id, "process");
755 }) || die ('error while saving shop order ' . $shop_order->{shop_ordernumber} . 'Error: ' . $shop_order->db->error . "\n" .
756 'generic exception:' . $@);
759 sub map_data_to_shoporder {
760 my ($self, $import) = @_;
762 croak "Expect a hash with one order." unless ref $import eq 'HASH';
763 # we need one number and a order date, some total prices and one customer
764 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
765 && $import->{orderDateTime}
766 && ref $import->{price} eq 'HASH'
767 && ref $import->{orderCustomer} eq 'HASH';
769 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
770 die "Cannot get shippingOrderAddressId for $import->{orderNumber}" unless $shipto_id;
772 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
773 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
774 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
776 croak("No Billing and ship to address, for Order Number " . $import->{orderNumber} .
777 "ID Billing:" . $import->{billingAddressId} . " ID Shipping $shipto_id ")
778 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
780 my $billing = $billing_ary->[0];
781 my $shipto = $shipto_ary->[0];
782 # TODO payment info is not used at all
783 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
785 croak "No billing city" unless $billing->{city};
786 croak "No shipto city" unless $shipto->{city};
787 croak "No customer email" unless $import->{orderCustomer}->{email};
790 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
792 time_zone => 'local' );
795 $orderdate = $parser->parse_datetime($import->{orderDateTime});
796 } catch { die "Cannot parse Order Date" . $_ };
798 my $shop_id = $self->config->id;
799 my $tax_included = $self->config->pricetype;
801 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
802 my %payment_ids_methods = (
803 # shopware_paymentId => kivitendo_payment_id
805 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
806 my $default_payment_id = $default_payment ? $default_payment->id : undef;
811 amount => $import->{amountTotal},
812 billing_city => $billing->{city},
813 billing_company => $billing->{company},
814 billing_country => $billing->{country}->{name},
815 billing_department => $billing->{department},
816 billing_email => $import->{orderCustomer}->{email},
817 billing_fax => $billing->{fax},
818 billing_firstname => $billing->{firstName},
819 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
820 billing_lastname => $billing->{lastName},
821 billing_phone => $billing->{phone},
822 billing_street => $billing->{street},
823 billing_vat => $billing->{vatId},
824 billing_zipcode => $billing->{zipcode},
825 customer_city => $billing->{city},
826 customer_company => $billing->{company},
827 customer_country => $billing->{country}->{name},
828 customer_department => $billing->{department},
829 customer_email => $billing->{email},
830 customer_fax => $billing->{fax},
831 customer_firstname => $billing->{firstName},
832 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
833 customer_lastname => $billing->{lastName},
834 customer_phone => $billing->{phoneNumber},
835 customer_street => $billing->{street},
836 customer_vat => $billing->{vatId},
837 customer_zipcode => $billing->{zipcode},
838 # customer_newsletter => $customer}->{newsletter},
839 delivery_city => $shipto->{city},
840 delivery_company => $shipto->{company},
841 delivery_country => $shipto->{country}->{name},
842 delivery_department => $shipto->{department},
843 delivery_email => "",
844 delivery_fax => $shipto->{fax},
845 delivery_firstname => $shipto->{firstName},
846 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
847 delivery_lastname => $shipto->{lastName},
848 delivery_phone => $shipto->{phone},
849 delivery_street => $shipto->{street},
850 delivery_vat => $shipto->{vatId},
851 delivery_zipcode => $shipto->{zipCode},
852 # host => $shop}->{hosts},
853 netamount => $import->{amountNet},
854 order_date => $orderdate,
855 payment_description => $payment->{name},
856 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
857 tax_included => $tax_included eq "brutto" ? 1 : 0,
858 shop_ordernumber => $import->{orderNumber},
860 shop_trans_id => $import->{id},
862 #remote_ip => $import->{remoteAddress},
863 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
864 #sepa_bic => $import->{paymentIntances}->{bic},
865 #sepa_iban => $import->{paymentIntances}->{iban},
866 #shipping_costs => $import->{invoiceShipping},
867 #shipping_costs_net => $import->{invoiceShippingNet},
868 #shop_c_billing_id => $import->{billing}->{customerId},
869 #shop_c_billing_number => $import->{billing}->{number},
870 #shop_c_delivery_id => $import->{shipping}->{id},
871 #shop_customer_id => $import->{customerId},
872 #shop_customer_number => $import->{billing}->{number},
873 #shop_customer_comment => $import->{customerComment},
876 my $shop_order = SL::DB::ShopOrder->new(%columns);
882 return encode('UTF-8', $value // '');
893 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
900 =head1 AVAILABLE METHODS
904 =item C<get_one_order>
906 =item C<get_new_orders>
910 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
912 The important key for shopware is the image name. To get distinct
913 entries the kivi partnumber is combined with the title (description)
914 of the image. Therefore part1000_someTitlefromUser should be unique in
916 All image data is simply send to shopware whether or not image data
917 has been edited recently.
918 If set_cover is set, the image with the position 1 will be used as
919 the shopware cover image.
920 If delete_orphaned ist set, all images related to the shopware product
921 which are not also in kivitendo will be deleted.
922 Shopware (6.4.x) takes care of deleting all the relations if the media
923 entry for the image is deleted.
924 More on media and Shopware6 can be found here:
925 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
930 =item C<get_categories>
934 Tries to establish a connection and in a second step
935 tries to get the server's version number.
936 Returns a hashref with the data structure the Base class expects.
938 =item C<set_orderstatus>
940 =item C<init_connector>
942 Inits the connection to the REST Server.
943 Errors are collected in $self->{errors} and undef will be returned.
944 If successful returns a REST::Client object for further communications.
950 L<SL::ShopConnector::ALL>
960 =item * Map all data to shop_order
962 Missing fields are commented in the sub map_data_to_shoporder.
963 Some items are SEPA debit info, IP adress, delivery costs etc
964 Furthermore Shopware6 uses currency, country and locales information.
966 =item * Use shipping_costs_parts_id for additional shipping costs
968 Currently dies if a shipping_costs_parts_id is set in the config
970 =item * Payment Infos can be read from shopware but is not linked with kivi
972 Unused data structures in sub map_data_to_shoporder => payment_ary
974 =item * Delete orphaned images is new in this connector, but should be in a separate method
976 =item * Fetch from last order number is ignored and should not be needed
978 Fetch orders also sets the state of the order from open to process. The state setting
979 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
980 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
981 and ignores any shopware order transition state.
983 =item * Get one order and get new orders is basically the same except for the filter
985 Right now the returning structure and the common parts of the filter are in two separate functions
989 Many error messages are thrown, but at least the more common cases should be localized.
995 Jan Büren jan@kivitendo.de