use Carp;
use Encode qw(encode);
+use List::Util qw(first);
use REST::Client;
use Try::Tiny;
use SL::JSON;
use SL::Helper::Flash;
+use SL::Locale::String qw(t8);
use Rose::Object::MakeMethods::Generic (
'scalar --get_set_init' => [ qw(connector) ],
my ($self, $shop_part, $todo) = @_;
#shop_part is passed as a param
- croak "Need a valid Shop Part for updating Part" unless ref($shop_part) eq 'SL::DB::ShopPart';
- croak "Invalid todo for updating Part" unless $todo =~ m/(price|stock|price_stock|active|all)/;
+ croak t8("Need a valid Shop Part for updating Part") unless ref($shop_part) eq 'SL::DB::ShopPart';
+ croak t8("Invalid todo for updating Part") unless $todo =~ m/(price|stock|price_stock|active|all)/;
my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
die "Shop Part but no kivi Part?" unless ref $part eq 'SL::DB::Part';
- my @cat = ();
- # if the part is connected to a category at all
- if ($shop_part->shop_category) {
- foreach my $row_cat ( @{ $shop_part->shop_category } ) {
- my $temp = { ( id => @{$row_cat}[0] ) };
- push ( @cat, $temp );
- }
- }
-
my $tax_n_price = $shop_part->get_tax_and_price;
my $price = $tax_n_price->{price};
my $taxrate = $tax_n_price->{tax};
my $update_p;
$update_p->{productNumber} = $part->partnumber;
- $update_p->{name} = $part->description;
+ $update_p->{name} = _u8($part->description);
+ $update_p->{description} = $shop_part->shop->use_part_longdescription
+ ? _u8($part->notes)
+ : _u8($shop_part->shop_description);
+
+ # locales simple check for english
+ my $english = SL::DB::Manager::Language->get_first(query => [ description => { ilike => 'Englisch' },
+ or => [ template_code => { ilike => 'en' } ],
+ ]);
+ if (ref $english eq 'SL::DB::Language') {
+ # add english translation for product
+ # TODO (or not): No Translations for shop_part->shop_description available
+ my $translation = first { $english->id == $_->language_id } @{ $part->translations };
+ $update_p->{translations}->{'en-GB'}->{name} = _u8($translation->{translation});
+ $update_p->{translations}->{'en-GB'}->{description} = _u8($translation->{longdescription});
+ }
$update_p->{stock} = $::form->round_amount($part->onhand, 0) if ($todo =~ m/(stock|all)/);
# JSON::true JSON::false
}
undef $update_p->{partNumber}; # we dont need this one
$ret = $self->connector->PATCH('api/product/' . $one_d->{id}, to_json($update_p));
- die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+ unless (204 == $ret->responseCode()) {
+ die t8('Part Description is too long for this Shopware version. It should have lower than 255 characters.')
+ if $ret->responseContent() =~ m/Diese Zeichenkette ist zu lang. Sie sollte.*255 Zeichen/;
+ die "Updating part with " . $part->partnumber . " failed: " . $ret->responseContent() unless (204 == $ret->responseCode());
+ }
} else {
# create part
# 1. get the correct tax for this product
$self->sync_all_images(shop_part => $shop_part, set_cover => 1, delete_orphaned => 1);
} catch { die "Could not sync images for Part " . $part->partnumber . " Reason: $_" };
+ # if there are categories try to sync this with the shop_part
+ try {
+ $self->sync_all_categories(shop_part => $shop_part);
+ } catch { die "Could not sync Categories for Part " . $part->partnumber . " Reason: $_" };
+
return 1; # no invalid response code -> success
}
+sub sync_all_categories {
+ my ($self, %params) = @_;
+
+ my $shop_part = delete $params{shop_part};
+ croak "Need a valid Shop Part for updating Images" unless ref($shop_part) eq 'SL::DB::ShopPart';
+
+ my $partnumber = $shop_part->part->partnumber;
+ die "Shop Part but no kivi Partnumber" unless $partnumber;
+
+ my ($ret, $response_code);
+ # 1 get uuid for product
+ my $product_filter = {
+ 'filter' => [
+ {
+ 'value' => $partnumber,
+ 'type' => 'equals',
+ 'field' => 'productNumber'
+ }
+ ]
+ };
+
+ $ret = $self->connector->POST('api/search/product', to_json($product_filter));
+ $response_code = $ret->responseCode();
+ die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 200;
+ my ($product_id, $category_tree);
+ try {
+ $product_id = from_json($ret->responseContent())->{data}->[0]->{id};
+ $category_tree = from_json($ret->responseContent())->{data}->[0]->{categoryIds};
+ } catch { die "Malformed JSON Data: $_ " . $ret->responseContent(); };
+ my $cat;
+ # if the part is connected to a category at all
+ if ($shop_part->shop_category) {
+ foreach my $row_cat (@{ $shop_part->shop_category }) {
+ $cat->{@{ $row_cat }[0]} = @{ $row_cat }[1];
+ }
+ }
+ # delete
+ foreach my $shopware_cat (@{ $category_tree }) {
+ if ($cat->{$shopware_cat}) {
+ # cat exists and no delete
+ delete $cat->{$shopware_cat};
+ next;
+ }
+ # cat exists and delete
+ $ret = $self->connector->DELETE("api/product/$product_id/categories/$shopware_cat");
+ $response_code = $ret->responseCode();
+ die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+ }
+ # now add only new categories
+ my $p;
+ $p->{id} = $product_id;
+ $p->{categories} = ();
+ foreach my $new_cat (keys %{ $cat }) {
+ push @{ $p->{categories} }, {id => $new_cat};
+ }
+ $ret = $self->connector->PATCH("api/product/$product_id", to_json($p));
+ $response_code = $ret->responseCode();
+ die "Request failed, response code was: $response_code\n" . $ret->responseContent() unless $response_code == 204;
+}
sub sync_all_images {
my ($self, %params) = @_;
sub get_one_order {
my ($self, $ordnumber) = @_;
- die "No ordnumber" unless $ordnumber;
+ croak t8("No Order Number") unless $ordnumber;
# set known params for the return structure
my %fetched_order = $self->get_fetched_order_structure;
my $assoc = $self->all_open_orders();
sub set_orderstatus {
my ($self, $order_id, $transition) = @_;
- croak "No order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
- croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
+ croak "No shop order ID, should be in format [0-9a-f]{32}" unless $order_id =~ m/^[0-9a-f]{32}$/;
+ croak "NO valid transition value" unless $transition =~ m/(open|process|cancel|complete)/;
my $ret;
$ret = $self->connector->POST("/api/_action/order/$order_id/state/$transition");
my $response_code = $ret->responseCode();
sub init_connector {
my ($self) = @_;
- my $client = REST::Client->new(host => $self->config->server);
+ my $protocol = $self->config->server =~ /(^https:\/\/|^http:\/\/)/ ? '' : $self->config->protocol . '://';
+ my $client = REST::Client->new(host => $protocol . $self->config->server);
+
+ $client->getUseragent()->proxy([$self->config->protocol], $self->config->proxy) if $self->config->proxy;
$client->addHeader('Content-Type', 'application/json');
$client->addHeader('charset', 'UTF-8');
$client->addHeader('Accept', 'application/json');
my ($self, $import) = @_;
# failsafe checks for not yet implemented
- die $::locale->text('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
+ die t8('Shipping cost article not implemented') if $self->config->shipping_costs_parts_id;
# no mapping unless we also have at least one shop order item ...
my $order_pos = delete $import->{lineItems};
- croak("No Order items fetched") unless ref $order_pos eq 'ARRAY';
+ croak t8("No Order items fetched") unless ref $order_pos eq 'ARRAY';
my $shop_order = $self->map_data_to_shoporder($import);
foreach my $pos (@positions) {
$position++;
my $price = $::form->round_amount($pos->{unitPrice}, 2); # unit
- my %pos_columns = ( description => $pos->{product}->{description},
- partnumber => $pos->{label},
+ my %pos_columns = ( description => $pos->{product}->{name},
+ partnumber => $pos->{product}->{productNumber},
price => $price,
quantity => $pos->{quantity},
position => $position,
$shop_order->positions($position);
if ( $self->config->shipping_costs_parts_id ) {
- die "Not yet implemented";
+ die t8("Not yet implemented");
# TODO NOT YET Implemented nor tested, this is shopware5 code:
my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
1;
- }) || die ('error while saving shop order ' . $shop_order->{shop_ordernumber} . 'Error: ' . $shop_order->db->error . "\n" .
- 'generic exception:' . $@);
+ }) || die t8('Error while saving shop order #1. DB Error #2. Generic exception #3.',
+ $shop_order->{shop_ordernumber}, $shop_order->db->error, $@);
}
sub map_data_to_shoporder {
&& ref $import->{orderCustomer} eq 'HASH';
my $shipto_id = $import->{deliveries}->[0]->{shippingOrderAddressId};
- die "Cannot get shippingOrderAddressId for $import->{orderNumber}" unless $shipto_id;
+ die t8("Cannot get shippingOrderAddressId for #1", $import->{orderNumber}) unless $shipto_id;
my $billing_ary = [ grep { $_->{id} == $import->{billingAddressId} } @{ $import->{addresses} } ];
my $shipto_ary = [ grep { $_->{id} == $shipto_id } @{ $import->{addresses} } ];
my $payment_ary = [ grep { $_->{id} == $import->{paymentMethodId} } @{ $import->{paymentMethods} } ];
- croak("No Billing and ship to address, for Order Number " . $import->{orderNumber} .
- "ID Billing:" . $import->{billingAddressId} . " ID Shipping $shipto_id ")
+ die t8("No Billing and ship to address, for Order Number #1 with ID Billing #2 and ID Shipping #3",
+ $import->{orderNumber}, $import->{billingAddressId}, $import->{deliveries}->[0]->{shippingOrderAddressId})
unless scalar @{ $billing_ary } == 1 && scalar @{ $shipto_ary } == 1;
my $billing = $billing_ary->[0];
# TODO payment info is not used at all
my $payment = scalar @{ $payment_ary } ? delete $payment_ary->[0] : undef;
- croak "No billing city" unless $billing->{city};
- croak "No shipto city" unless $shipto->{city};
- croak "No customer email" unless $import->{orderCustomer}->{email};
+ # check mandatory fields from shopware
+ die t8("No billing city") unless $billing->{city};
+ die t8("No shipto city") unless $shipto->{city};
+ die t8("No customer email") unless $import->{orderCustomer}->{email};
# extract order date
my $parser = DateTime::Format::Strptime->new(pattern => '%Y-%m-%dT%H:%M:%S',
my $shop_id = $self->config->id;
my $tax_included = $self->config->pricetype;
+ # TODO copied from shopware5 connector
# Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
my %payment_ids_methods = (
# shopware_paymentId => kivitendo_payment_id
=item C<update_part>
+Updates all metadata for a shop part. See base class for a general description.
+Specific Implementation notes:
+=over 4
+
+=item Calls sync_all_images with set_cover = 1 and delete_orphaned = 1
+
+=item Checks if longdescription should be taken from part or shop_part
+
+=item Checks if a language with the name 'Englisch' or template_code 'en'
+ is available and sets the shopware6 'en-GB' locales for the product
+
=item C<sync_all_images (set_cover: 0|1, delete_orphaned: 0|1)>
-The important key for shopware is the image name. To get distinct
-entries the kivi partnumber is combined with the title (description)
+The connecting key for shopware to kivi images is the image name.
+To get distinct entries the kivi partnumber is combined with the title (description)
of the image. Therefore part1000_someTitlefromUser should be unique in
Shopware.
All image data is simply send to shopware whether or not image data
More on media and Shopware6 can be found here:
https://shopware.stoplight.io/docs/admin-api/ZG9jOjEyNjI1Mzkw-media-handling
+=back
+
+=over 4
=item C<get_article>
Missing fields are commented in the sub map_data_to_shoporder.
Some items are SEPA debit info, IP adress, delivery costs etc
Furthermore Shopware6 uses currency, country and locales information.
+Detailed list:
+
+ #customer_newsletter => $customer}->{newsletter},
+ #remote_ip => $import->{remoteAddress},
+ #sepa_account_holder => $import->{paymentIntances}->{accountHolder},
+ #sepa_bic => $import->{paymentIntances}->{bic},
+ #sepa_iban => $import->{paymentIntances}->{iban},
+ #shipping_costs => $import->{invoiceShipping},
+ #shipping_costs_net => $import->{invoiceShippingNet},
+ #shop_c_billing_id => $import->{billing}->{customerId},
+ #shop_c_billing_number => $import->{billing}->{number},
+ #shop_c_delivery_id => $import->{shipping}->{id},
+ #shop_customer_id => $import->{customerId},
+ #shop_customer_number => $import->{billing}->{number},
+ #shop_customer_comment => $import->{customerComment},
=item * Use shipping_costs_parts_id for additional shipping costs
Many error messages are thrown, but at least the more common cases should be localized.
+=item * Multi language support
+
+By guessing the correct german name for the english language some translation for parts can
+also be synced. This should be more clear (language configuration for shops) and the order
+synchronisation should also handle this (longdescription is simply copied from part.notes)
+
=back
=head1 AUTHOR