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;
141 $update_p->{description} = $shop_part->shop->use_part_longdescription
143 : $shop_part->shop_description;
146 $update_p->{stock} = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
147 # JSON::true JSON::false
148 # These special values become JSON true and JSON false values, respectively.
149 # You can also use \1 and \0 directly if you want
150 $update_p->{active} = (!$part->obsolete && $part->shop) ? \1 : \0 if ($todo =~ m/(active|all)/);
152 # 1. check if there is already a product
153 my $product_filter = {
156 'value' => $part->partnumber,
158 'field' => 'productNumber'
162 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
163 my $response_code = $ret->responseCode();
164 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
166 my $one_d; # maybe empty
168 $one_d = from_json($ret->responseContent())->{data}->[0];
169 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
170 # edit or create if not found
173 # we need price object structure and taxId
174 $update_p->{$_} = $one_d->{$_} foreach qw(taxId price);
175 if ($todo =~ m/(price|all)/) {
176 $update_p->{price}->[0]->{gross} = $gross;
178 undef $update_p->{partNumber}; # we dont need this one
179 $ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
180 die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
183 # 1. get the correct tax for this product
193 $ret = $self->connector->POST('api/search/tax', to_json($tax_filter));
194 die "Search for Tax with rate: " . $part->partnumber . " failed: " . $ret->responseContent() unless (200 == $ret->responseCode());
196 $update_p->{taxId} = from_json($ret->responseContent())->{data}->[0]->{id};
197 } catch { die "Malformed JSON Data or Taxkey entry missing: $_ " . $ret->responseContent(); };
199 # 2. get the correct currency for this product
200 my $currency_filter = {
203 'value' => SL::DB::Default->get_default_currency,
209 $ret = $self->connector->POST('api/search/currency', to_json($currency_filter));
210 die "Search for Currency with ISO Code: " . SL::DB::Default->get_default_currency . " failed: "
211 . $ret->responseContent() unless (200 == $ret->responseCode());
214 $update_p->{price}->[0]->{currencyId} = from_json($ret->responseContent())->{data}->[0]->{id};
215 } catch { die "Malformed JSON Data or Currency ID entry missing: $_ " . $ret->responseContent(); };
217 # 3. add net and gross price and allow variants
218 $update_p->{price}->[0]->{gross} = $gross;
219 $update_p->{price}->[0]->{net} = $net;
220 $update_p->{price}->[0]->{linked} = \1; # link product variants
222 $ret = $self->connector->POST('api/product', to_json($update_p));
223 die "Create for Product " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
226 # if there are images try to sync this with the shop_part
228 $self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
229 } catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
231 return 1; # no invalid response code -> success
234 sub sync_all_images {
235 my ($self, %params) = @_;
237 $params{set_cover} //= 1;
238 $params{delete_orphaned} //= 0;
240 my $shop_part = delete $params{shop_part};
241 croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
243 my $partnumber = $shop_part->part->partnumber;
244 die "Shop Part but no kivi Partnumber" unless $partnumber;
246 my @upload_img = $shop_part->get_images(want_binary => 1);
248 return unless (@upload_img); # there are no images, but delete wont work TODO extract to method
250 my ($ret, $response_code);
251 # 1. get part uuid and get media associations
252 # 2. create or update the media entry for the filename
253 # 2.1 if no media entry exists create one
255 # 2.2 create or update media_product and set position
256 # 3. optional set cover image
257 # 4. optional delete images in shopware which are not in kivi
259 # 1 get mediaid uuid for prodcut
260 my $product_filter = {
266 'value' => $partnumber,
268 'field' => 'productNumber'
273 $ret = $self->connector->POST('api/search/product', to_json($product_filter));
274 $response_code = $ret->responseCode();
275 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
276 my ($product_id, $media_data);
278 $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
279 # $media_data = from_json($ret->responseContent())->{data}->[0]->{media};
280 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
282 # 2 iterate all kivi images and save distinct name for later sync
284 foreach my $img (@upload_img) {
285 die $::locale->text("Need a image title") unless $img->{description};
286 my $distinct_media_name = $partnumber . '_' . $img->{description};
287 $existing_images{$distinct_media_name} = 1;
288 my $image_filter = { 'filter' => [
290 'value' => $distinct_media_name,
292 'field' => 'fileName'
296 $ret = $self->connector->POST('api/search/media', to_json($image_filter));
297 $response_code = $ret->responseCode();
298 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
299 my $current_image_id; # maybe empty
301 $current_image_id = from_json($ret->responseContent())->{data}->[0]->{id};
302 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
304 # 2.1 no image with this title, create metadata for media and upload image
305 if (!$current_image_id) {
306 # not yet uploaded, create media entry
307 $ret = $self->connector->POST("/api/media?_response=true");
308 $response_code = $ret->responseCode();
309 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
311 $current_image_id = from_json($ret->responseContent())->{data}{id};
312 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
314 # 2.2 update the image data (current_image_id was found or created)
315 $ret = $self->connector->POST("/api/_action/media/$current_image_id/upload?fileName=$distinct_media_name&extension=$img->{extension}",
318 "Content-Type" => "image/$img->{extension}",
320 $response_code = $ret->responseCode();
321 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
323 # 2.3 check if a product media entry exists for this id
324 my $product_media_filter = {
327 'value' => $product_id,
329 'field' => 'productId'
332 'value' => $current_image_id,
338 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
339 $response_code = $ret->responseCode();
340 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
341 my ($has_product_media, $product_media_id);
343 $has_product_media = from_json($ret->responseContent())->{total};
344 $product_media_id = from_json($ret->responseContent())->{data}->[0]->{id};
345 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
347 # 2.4 ... and either update or create the entry
348 # set shopware position to kivi position
350 $product_media->{position} = $img->{position}; # position may change
352 if ($has_product_media == 0) {
353 # 2.4.1 new entry. link product to media
354 $product_media->{productId} = $product_id;
355 $product_media->{mediaId} = $current_image_id;
356 $ret = $self->connector->POST('api/product-media', to_json($product_media));
357 } elsif ($has_product_media == 1 && $product_media_id) {
358 $ret = $self->connector->PATCH("api/product-media/$product_media_id", to_json($product_media));
360 die "Invalid state, please inform Shopware master admin at product-media filter: $product_media_filter";
362 $response_code = $ret->responseCode();
363 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
365 # 3. optional set image with position 1 as cover image
366 if ($params{set_cover}) {
367 # set cover if position == 1
368 my $product_media_filter = {
371 'value' => $product_id,
373 'field' => 'productId'
378 'field' => 'position'
383 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
384 $response_code = $ret->responseCode();
385 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
388 $cover->{coverId} = from_json($ret->responseContent())->{data}->[0]->{id};
389 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
390 $ret = $self->connector->PATCH('api/product/' . $product_id, to_json($cover));
391 $response_code = $ret->responseCode();
392 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
394 # 4. optional delete orphaned images in shopware
395 if ($params{delete_orphaned}) {
396 # delete orphaned images
397 my $product_media_filter = {
400 'value' => $product_id,
402 'field' => 'productId'
404 $ret = $self->connector->POST('api/search/product-media', to_json($product_media_filter));
405 $response_code = $ret->responseCode();
406 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
409 $img_ary = from_json($ret->responseContent())->{data};
410 } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
412 if (scalar @{ $img_ary} > 0) { # maybe no images at all
414 $existing_img{$_->{media}->{fileName}}= $_->{media}->{id} for @{ $img_ary };
416 while (my ($name, $id) = each %existing_img) {
417 next if $existing_images{$name};
418 $ret = $self->connector->DELETE("api/media/$id");
419 $response_code = $ret->responseCode();
420 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
430 my $ret = $self->connector->POST('api/search/category');
431 my $response_code = $ret->responseCode();
433 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
437 $import = decode_json $ret->responseContent();
439 die "Malformed JSON Data: $_ " . $ret->responseContent();
442 my @daten = @{ $import->{data} };
443 my %categories = map { ($_->{id} => $_) } @daten;
447 my $parent = $categories{$_->{parentId}};
449 $parent->{children} ||= [];
450 push @{ $parent->{children} }, $_;
452 push @categories_tree, $_;
455 return \@categories_tree;
459 my ($self, $ordnumber) = @_;
461 croak t8("No Order Number") unless $ordnumber;
462 # set known params for the return structure
463 my %fetched_order = $self->get_fetched_order_structure;
464 my $assoc = $self->all_open_orders();
466 # overwrite filter for exactly one ordnumber
467 $assoc->{filter}->[0]->{value} = $ordnumber;
468 $assoc->{filter}->[0]->{type} = 'equals';
469 $assoc->{filter}->[0]->{field} = 'orderNumber';
471 # 1. fetch the order and import it as a kivi order
472 # 2. return the number of processed order (1)
473 my $one_order = $self->connector->POST('api/search/order', to_json($assoc));
475 # 1. check for bad request or connection problems
476 if ($one_order->responseCode() != 200) {
477 $fetched_order{error} = 1;
478 $fetched_order{message} = $one_order->responseCode() . ' ' . $one_order->responseContent();
479 return \%fetched_order;
482 # 1.1 parse json or exit
485 $content = from_json($one_order->responseContent());
487 $fetched_order{error} = 1;
488 $fetched_order{message} = "Malformed JSON Data: $_ " . $one_order->responseContent();
489 return \%fetched_order;
492 # 2. check if we found ONE order at all
493 my $total = $content->{total};
495 $fetched_order{number_of_orders} = 0;
496 return \%fetched_order;
497 } elsif ($total != 1) {
498 $fetched_order{error} = 1;
499 $fetched_order{message} = "More than one Order returned. Invalid State: $total";
500 return \%fetched_order;
503 # 3. there is one valid order, try to import this one
504 if ($self->import_data_to_shop_order($content->{data}->[0])) {
505 %fetched_order = (shop_description => $self->config->description, number_of_orders => 1);
507 $fetched_order{message} = "Error: $@";
508 $fetched_order{error} = 1;
510 return \%fetched_order;
516 my %fetched_order = $self->get_fetched_order_structure;
517 my $assoc = $self->all_open_orders();
519 # 1. fetch all open orders and try to import it as a kivi order
520 # 2. return the number of processed order $total
521 my $open_orders = $self->connector->POST('api/search/order', to_json($assoc));
523 # 1. check for bad request or connection problems
524 if ($open_orders->responseCode() != 200) {
525 $fetched_order{error} = 1;
526 $fetched_order{message} = $open_orders->responseCode() . ' ' . $open_orders->responseContent();
527 return \%fetched_order;
530 # 1.1 parse json or exit
533 $content = from_json($open_orders->responseContent());
535 $fetched_order{error} = 1;
536 $fetched_order{message} = "Malformed JSON Data: $_ " . $open_orders->responseContent();
537 return \%fetched_order;
540 # 2. check if we found one or more order at all
541 my $total = $content->{total};
543 $fetched_order{number_of_orders} = 0;
544 return \%fetched_order;
545 } elsif (!$total || !($total > 0)) {
546 $fetched_order{error} = 1;
547 $fetched_order{message} = "Undefined value for total orders returned. Invalid State: $total";
548 return \%fetched_order;
551 # 3. there are open orders. try to import one by one
552 $fetched_order{number_of_orders} = 0;
553 foreach my $open_order (@{ $content->{data} }) {
554 if ($self->import_data_to_shop_order($open_order)) {
555 $fetched_order{number_of_orders}++;
557 $fetched_order{message} .= "Error at importing order with running number:"
558 . $fetched_order{number_of_orders}+1 . ": $@ \n";
559 $fetched_order{error} = 1;
562 return \%fetched_order;
566 my ($self, $partnumber) = @_;
568 $partnumber = $::form->escape($partnumber);
569 my $product_filter = {
572 'value' => $partnumber,
574 'field' => 'productNumber'
578 my $ret = $self->connector->POST('api/search/product', to_json($product_filter));
580 my $response_code = $ret->responseCode();
581 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
585 $data_json = decode_json $ret->responseContent();
587 die "Malformed JSON Data: $_ " . $ret->responseContent();
590 # maybe no product was found ...
591 return undef unless scalar @{ $data_json->{data} } > 0;
592 # caller wants this structure:
593 # $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
594 # $active_online = $shop_article->{data}->{active};
596 $data->{data}->{mainDetail}->{inStock} = $data_json->{data}->[0]->{stock};
597 $data->{data}->{active} = $data_json->{data}->[0]->{active};
604 my $return = {}; # return for caller
605 my $ret = {}; # internal return
607 # 1. check if we can connect at all
608 # 2. request version number
610 $ret = $self->connector;
611 if (200 != $ret->responseCode()) {
612 $return->{success} = 0;
613 $return->{data}->{version} = $self->{errors}; # whatever init puts in errors
617 $ret = $self->connector->GET('api/_info/version');
618 if (200 == $ret->responseCode()) {
619 my $version = from_json($self->connector->responseContent())->{version};
620 $return->{success} = 1;
621 $return->{data}->{version} = $version;
623 $return->{success} = 0;
624 $return->{data}->{version} = $ret->responseContent(); # Whatever REST Interface says
630 sub set_orderstatus {
631 my ($self, $order_id, $transition) = @_;
633 croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
634 croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
636 $ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
637 my $response_code = $ret->responseCode();
638 die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code eq '200';
645 my $client = REST::Client->new(host => $self->config->server);
646 $client->addHeader('Content-Type', 'application/json');
647 $client->addHeader('charset', 'UTF-8');
648 $client->addHeader('Accept', 'application/json');
651 client_id => $self->config->login,
652 client_secret => $self->config->password,
653 grant_type => "client_credentials",
656 my $ret = $client->POST('/api/oauth/token', encode_json(\%auth_req));
658 unless (200 == $ret->responseCode()) {
659 $self->{errors} .= $ret->responseContent();
663 my $token = from_json($client->responseContent())->{access_token};
665 $self->{errors} .= "No Auth-Token received";
668 # persist refresh token
669 $client->addHeader('Authorization' => 'Bearer ' . $token);
673 sub import_data_to_shop_order {
674 my ($self, $import) = @_;
676 # failsafe checks for not yet implemented
677 die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
679 # no mapping unless we also have at least one shop order item ...
680 my $order_pos = delete $import->{lineItems};
681 croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
683 my $shop_order = $self->map_data_to_shoporder($import);
685 my $shop_transaction_ret = $shop_order->db->with_transaction(sub {
687 my $id = $shop_order->id;
689 my @positions = sort { Sort::Naturally::ncmp($a->{"label"}, $b->{"label"}) } @{ $order_pos };
691 my $active_price_source = $self->config->price_source;
693 foreach my $pos (@positions) {
695 my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
696 my %pos_columns = ( description => $pos->{product}->{description},
697 partnumber => $pos->{label},
699 quantity => $pos->{quantity},
700 position => $position,
701 tax_rate => $pos->{priceDefinition}->{taxRules}->[0]->{taxRate},
702 shop_trans_id => $pos->{id}, # pos id or shop_trans_id ? or dont care?
703 shop_order_id => $id,
704 active_price_source => $active_price_source,
706 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
709 $shop_order->positions($position);
711 if ( $self->config->shipping_costs_parts_id ) {
712 die t8("Not yet implemented");
713 # TODO NOT YET Implemented nor tested, this is shopware5 code:
714 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
715 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
716 partnumber => $shipping_part->partnumber,
717 price => $import->{data}->{invoiceShipping},
719 position => $position,
721 shop_order_id => $id,
723 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
724 $shipping_pos_insert->save;
727 my $customer = $shop_order->get_customer;
729 if (ref $customer eq 'SL::DB::Customer') {
730 $shop_order->kivi_customer_id($customer->id);
734 # update state in shopware before transaction ends
735 $self->set_orderstatus($shop_order->shop_trans_id, "process");
739 }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
740 $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
743 sub map_data_to_shoporder {
744 my ($self, $import) = @_;
746 croak "Expect a hash with one order." unless ref $import eq 'HASH';
747 # we need one number and a order date, some total prices and one customer
748 croak "Does not look like a shopware6 order" unless $import->{orderNumber}
749 && $import->{orderDateTime}
750 && ref $import->{price} eq 'HASH'
751 && ref $import->{orderCustomer} eq 'HASH';
753 my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
754 die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
756 my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
757 my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
758 my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
760 die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
761 $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
762 unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
764 my $billing = $billing_ary->[0];
765 my $shipto = $shipto_ary->[0];
766 # TODO payment info is not used at all
767 my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
769 # check mandatory fields from shopware
770 die t8("No billing city") unless $billing->{city};
771 die t8("No shipto city") unless $shipto->{city};
772 die t8("No customer email") unless $import->{orderCustomer}->{email};
775 my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
777 time_zone => 'local' );
780 $orderdate = $parser->parse_datetime($import->{orderDateTime});
781 } catch { die "Cannot parse Order Date" . $_ };
783 my $shop_id = $self->config->id;
784 my $tax_included = $self->config->pricetype;
786 # TODO copied from shopware5 connector
787 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
788 my %payment_ids_methods = (
789 # shopware_paymentId => kivitendo_payment_id
791 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
792 my $default_payment_id = $default_payment ? $default_payment->id : undef;
797 amount => $import->{amountTotal},
798 billing_city => $billing->{city},
799 billing_company => $billing->{company},
800 billing_country => $billing->{country}->{name},
801 billing_department => $billing->{department},
802 billing_email => $import->{orderCustomer}->{email},
803 billing_fax => $billing->{fax},
804 billing_firstname => $billing->{firstName},
805 #billing_greeting => ($import->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
806 billing_lastname => $billing->{lastName},
807 billing_phone => $billing->{phone},
808 billing_street => $billing->{street},
809 billing_vat => $billing->{vatId},
810 billing_zipcode => $billing->{zipcode},
811 customer_city => $billing->{city},
812 customer_company => $billing->{company},
813 customer_country => $billing->{country}->{name},
814 customer_department => $billing->{department},
815 customer_email => $billing->{email},
816 customer_fax => $billing->{fax},
817 customer_firstname => $billing->{firstName},
818 #customer_greeting => ($billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
819 customer_lastname => $billing->{lastName},
820 customer_phone => $billing->{phoneNumber},
821 customer_street => $billing->{street},
822 customer_vat => $billing->{vatId},
823 customer_zipcode => $billing->{zipcode},
824 # customer_newsletter => $customer}->{newsletter},
825 delivery_city => $shipto->{city},
826 delivery_company => $shipto->{company},
827 delivery_country => $shipto->{country}->{name},
828 delivery_department => $shipto->{department},
829 delivery_email => "",
830 delivery_fax => $shipto->{fax},
831 delivery_firstname => $shipto->{firstName},
832 #delivery_greeting => ($shipto}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
833 delivery_lastname => $shipto->{lastName},
834 delivery_phone => $shipto->{phone},
835 delivery_street => $shipto->{street},
836 delivery_vat => $shipto->{vatId},
837 delivery_zipcode => $shipto->{zipCode},
838 # host => $shop}->{hosts},
839 netamount => $import->{amountNet},
840 order_date => $orderdate,
841 payment_description => $payment->{name},
842 payment_id => $payment_ids_methods{$import->{paymentId}} || $default_payment_id,
843 tax_included => $tax_included eq "brutto" ? 1 : 0,
844 shop_ordernumber => $import->{orderNumber},
846 shop_trans_id => $import->{id},
848 #remote_ip => $import->{remoteAddress},
849 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
850 #sepa_bic => $import->{paymentIntances}->{bic},
851 #sepa_iban => $import->{paymentIntances}->{iban},
852 #shipping_costs => $import->{invoiceShipping},
853 #shipping_costs_net => $import->{invoiceShippingNet},
854 #shop_c_billing_id => $import->{billing}->{customerId},
855 #shop_c_billing_number => $import->{billing}->{number},
856 #shop_c_delivery_id => $import->{shipping}->{id},
857 #shop_customer_id => $import->{customerId},
858 #shop_customer_number => $import->{billing}->{number},
859 #shop_customer_comment => $import->{customerComment},
862 my $shop_order = SL::DB::ShopOrder->new(%columns);
868 return encode('UTF-8', $value // '');
879 SL::ShopConnector::Shopware6 - this is the Connector Class for Shopware 6
886 =head1 AVAILABLE METHODS
890 =item C<get_one_order>
892 =item C<get_new_orders>
896 =item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
898 The connecting key for shopware to kivi images is the image name.
899 To get distinct entries the kivi partnumber is combined with the title (description)
900 of the image. Therefore part1000_someTitlefromUser should be unique in
902 All image data is simply send to shopware whether or not image data
903 has been edited recently.
904 If set_cover is set, the image with the position 1 will be used as
905 the shopware cover image.
906 If delete_orphaned ist set, all images related to the shopware product
907 which are not also in kivitendo will be deleted.
908 Shopware (6.4.x) takes care of deleting all the relations if the media
909 entry for the image is deleted.
910 More on media and Shopware6 can be found here:
911 https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
916 =item C<get_categories>
920 Tries to establish a connection and in a second step
921 tries to get the server's version number.
922 Returns a hashref with the data structure the Base class expects.
924 =item C<set_orderstatus>
926 =item C<init_connector>
928 Inits the connection to the REST Server.
929 Errors are collected in $self->{errors} and undef will be returned.
930 If successful returns a REST::Client object for further communications.
936 L<SL::ShopConnector::ALL>
946 =item * Map all data to shop_order
948 Missing fields are commented in the sub map_data_to_shoporder.
949 Some items are SEPA debit info, IP adress, delivery costs etc
950 Furthermore Shopware6 uses currency, country and locales information.
953 #customer_newsletter => $customer}->{newsletter},
954 #remote_ip => $import->{remoteAddress},
955 #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
956 #sepa_bic => $import->{paymentIntances}->{bic},
957 #sepa_iban => $import->{paymentIntances}->{iban},
958 #shipping_costs => $import->{invoiceShipping},
959 #shipping_costs_net => $import->{invoiceShippingNet},
960 #shop_c_billing_id => $import->{billing}->{customerId},
961 #shop_c_billing_number => $import->{billing}->{number},
962 #shop_c_delivery_id => $import->{shipping}->{id},
963 #shop_customer_id => $import->{customerId},
964 #shop_customer_number => $import->{billing}->{number},
965 #shop_customer_comment => $import->{customerComment},
967 =item * Use shipping_costs_parts_id for additional shipping costs
969 Currently dies if a shipping_costs_parts_id is set in the config
971 =item * Payment Infos can be read from shopware but is not linked with kivi
973 Unused data structures in sub map_data_to_shoporder => payment_ary
975 =item * Delete orphaned images is new in this connector, but should be in a separate method
977 =item * Fetch from last order number is ignored and should not be needed
979 Fetch orders also sets the state of the order from open to process. The state setting
980 is transaction safe and therefore get_new_orders should always fetch only unprocessed orders
981 at all. Nevertheless get_one_order just gets one order with the exactly matching order number
982 and ignores any shopware order transition state.
984 =item * Get one order and get new orders is basically the same except for the filter
986 Right now the returning structure and the common parts of the filter are in two separate functions
990 Many error messages are thrown, but at least the more common cases should be localized.
996 Jan Büren jan@kivitendo.de