1 package SL::ShopConnector::Shopware;
5 use parent qw(SL::ShopConnector::Base);
10 use LWP::Authen::Digest;
11 use SL::DB::ShopOrder;
12 use SL::DB::ShopOrderItem;
14 use SL::DB::PaymentTerm;
15 use DateTime::Format::Strptime;
18 use Sort::Naturally ();
19 use SL::Helper::Flash;
20 use Encode qw(encode_utf8);
24 use Rose::Object::MakeMethods::Generic (
25 'scalar --get_set_init' => [ qw(connector url) ],
29 my ($self, $ordnumber) = @_;
31 my $dbh = SL::DB::client;
34 my $data = $self->connector->get($url . "api/orders/$ordnumber?useNumberAsId=true");
38 if ($data->is_success && $data->content_type eq 'application/json'){
39 my $data_json = $data->content;
40 my $import = SL::JSON::decode_json($data_json);
41 my $shoporder = $import->{data};
42 $dbh->with_transaction( sub{
43 $self->import_data_to_shop_order($import);
46 push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
50 $self->set_orderstatus($import->{data}->{id}, "fetched");
53 flash_later('error', $::locale->text('Database errors: #1', @errors));
55 %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
58 shop_id => $self->config->id,
59 shop_description => $self->config->description,
60 message => "Error: $data->status_line",
63 %fetched_orders = %error_msg;
66 return \%fetched_orders;
73 my $last_order_number = $self->config->last_order_number;
74 my $otf = $self->config->orders_to_fetch;
76 my $last_data = $self->connector->get($url . "api/orders/$last_order_number?useNumberAsId=true");
77 my $last_data_json = $last_data->content;
78 my $last_import = SL::JSON::decode_json($last_data_json);
80 my $orders_data = $self->connector->get($url . "api/orders?limit=$otf&filter[1][property]=status&filter[1][value]=0&filter[0][property]=id&filter[0][expression]=>&filter[0][value]=" . $last_import->{data}->{id});
82 my $dbh = SL::DB->client;
85 if ($orders_data->is_success && $orders_data->content_type eq 'application/json'){
86 my $orders_data_json = $orders_data->content;
87 my $orders_import = SL::JSON::decode_json($orders_data_json);
88 foreach my $shoporder(@{ $orders_import->{data} }){
90 my $data = $self->connector->get($url . "api/orders/" . $shoporder->{id});
91 my $data_json = $data->content;
92 my $import = SL::JSON::decode_json($data_json);
94 $dbh->with_transaction( sub{
95 $self->import_data_to_shop_order($import);
97 $self->config->assign_attributes( last_order_number => $shoporder->{number});
101 push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
105 $self->set_orderstatus($shoporder->{id}, "fetched");
108 flash_later('error', $::locale->text('Database errors: #1', @errors));
111 %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
114 shop_id => $self->config->id,
115 shop_description => $self->config->description,
116 message => "Error: $orders_data->status_line",
119 %fetched_orders = %error_msg;
122 return \%fetched_orders;
125 sub import_data_to_shop_order {
126 my ( $self, $import ) = @_;
127 my $shop_order = $self->map_data_to_shoporder($import);
130 my $id = $shop_order->id;
132 my @positions = sort { Sort::Naturally::ncmp($a->{"articleNumber"}, $b->{"articleNumber"}) } @{ $import->{data}->{details} };
133 #my @positions = sort { Sort::Naturally::ncmp($a->{"partnumber"}, $b->{"partnumber"}) } @{ $import->{data}->{details} };
135 my $active_price_source = $self->config->price_source;
137 foreach my $pos(@positions) {
138 my $price = $::form->round_amount($pos->{price},2);
139 my %pos_columns = ( description => $pos->{articleName},
140 partnumber => $pos->{articleNumber},
142 quantity => $pos->{quantity},
143 position => $position,
144 tax_rate => $pos->{taxRate},
145 shop_trans_id => $pos->{articleId},
146 shop_order_id => $id,
147 active_price_source => $active_price_source,
149 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
153 $shop_order->positions($position-1);
155 if ( $self->config->shipping_costs_parts_id ) {
156 my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
157 my %shipping_pos = ( description => $import->{data}->{dispatch}->{name},
158 partnumber => $shipping_part->partnumber,
159 price => $import->{data}->{invoiceShipping},
161 position => $position,
163 shop_order_id => $id,
165 my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
166 $shipping_pos_insert->save;
169 my $customer = $shop_order->get_customer;
172 $shop_order->kivi_customer_id($customer->id);
177 sub map_data_to_shoporder {
178 my ($self, $import) = @_;
180 my $parser = DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S',
184 my $orderdate = $parser->parse_datetime($import->{data}->{orderTime});
186 my $shop_id = $self->config->id;
187 my $tax_included = $self->config->pricetype;
189 # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
190 my %payment_ids_methods = (
191 # shopware_paymentId => kivitendo_payment_id
193 my $default_payment = SL::DB::Manager::PaymentTerm->get_first();
194 my $default_payment_id = $default_payment ? $default_payment->id : undef;
195 # Mapping to table shoporders. See http://community.shopware.com/_detail_1690.html#GET_.28Liste.29
197 amount => $import->{data}->{invoiceAmount},
198 billing_city => $import->{data}->{billing}->{city},
199 billing_company => $import->{data}->{billing}->{company},
200 billing_country => $import->{data}->{billing}->{country}->{name},
201 billing_department => $import->{data}->{billing}->{department},
202 billing_email => $import->{data}->{customer}->{email},
203 billing_fax => $import->{data}->{billing}->{fax},
204 billing_firstname => $import->{data}->{billing}->{firstName},
205 #billing_greeting => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
206 billing_lastname => $import->{data}->{billing}->{lastName},
207 billing_phone => $import->{data}->{billing}->{phone},
208 billing_street => $import->{data}->{billing}->{street},
209 billing_vat => $import->{data}->{billing}->{vatId},
210 billing_zipcode => $import->{data}->{billing}->{zipCode},
211 customer_city => $import->{data}->{billing}->{city},
212 customer_company => $import->{data}->{billing}->{company},
213 customer_country => $import->{data}->{billing}->{country}->{name},
214 customer_department => $import->{data}->{billing}->{department},
215 customer_email => $import->{data}->{customer}->{email},
216 customer_fax => $import->{data}->{billing}->{fax},
217 customer_firstname => $import->{data}->{billing}->{firstName},
218 #customer_greeting => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
219 customer_lastname => $import->{data}->{billing}->{lastName},
220 customer_phone => $import->{data}->{billing}->{phone},
221 customer_street => $import->{data}->{billing}->{street},
222 customer_vat => $import->{data}->{billing}->{vatId},
223 customer_zipcode => $import->{data}->{billing}->{zipCode},
224 customer_newsletter => $import->{data}->{customer}->{newsletter},
225 delivery_city => $import->{data}->{shipping}->{city},
226 delivery_company => $import->{data}->{shipping}->{company},
227 delivery_country => $import->{data}->{shipping}->{country}->{name},
228 delivery_department => $import->{data}->{shipping}->{department},
229 delivery_email => "",
230 delivery_fax => $import->{data}->{shipping}->{fax},
231 delivery_firstname => $import->{data}->{shipping}->{firstName},
232 #delivery_greeting => ($import->{data}->{shipping}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
233 delivery_lastname => $import->{data}->{shipping}->{lastName},
234 delivery_phone => $import->{data}->{shipping}->{phone},
235 delivery_street => $import->{data}->{shipping}->{street},
236 delivery_vat => $import->{data}->{shipping}->{vatId},
237 delivery_zipcode => $import->{data}->{shipping}->{zipCode},
238 host => $import->{data}->{shop}->{hosts},
239 netamount => $import->{data}->{invoiceAmountNet},
240 order_date => $orderdate,
241 payment_description => $import->{data}->{payment}->{description},
242 payment_id => $payment_ids_methods{$import->{data}->{paymentId}} || $default_payment_id,
243 remote_ip => $import->{data}->{remoteAddress},
244 sepa_account_holder => $import->{data}->{paymentIntances}->{accountHolder},
245 sepa_bic => $import->{data}->{paymentIntances}->{bic},
246 sepa_iban => $import->{data}->{paymentIntances}->{iban},
247 shipping_costs => $import->{data}->{invoiceShipping},
248 shipping_costs_net => $import->{data}->{invoiceShippingNet},
249 shop_c_billing_id => $import->{data}->{billing}->{customerId},
250 shop_c_billing_number => $import->{data}->{billing}->{number},
251 shop_c_delivery_id => $import->{data}->{shipping}->{id},
252 shop_customer_id => $import->{data}->{customerId},
253 shop_customer_number => $import->{data}->{billing}->{number},
254 shop_customer_comment => $import->{data}->{customerComment},
256 shop_ordernumber => $import->{data}->{number},
257 shop_trans_id => $import->{data}->{id},
258 tax_included => $tax_included eq "brutto" ? 1 : 0,
261 my $shop_order = SL::DB::ShopOrder->new(%columns);
268 my $url = $self->url;
269 my $data = $self->connector->get($url . "api/categories");
270 my $data_json = $data->content;
271 my $import = SL::JSON::decode_json($data_json);
272 my @daten = @{$import->{data}};
273 my %categories = map { ($_->{id} => $_) } @daten;
277 # ignore root with id=1
281 my $parent = $categories{$_->{parentId}};
282 if($parent && $parent->{id} != 1) {
283 $parent->{children} ||= [];
284 push @{$parent->{children}},$_;
286 push @categories_tree, $_;
290 return \@categories_tree;
296 my $url = $self->url;
297 my $data = $self->connector->get($url . "api/version");
298 my $type = $data->content_type;
299 my $status_line = $data->status_line;
301 if($data->is_success && $type eq 'application/json'){
302 my $data_json = $data->content;
303 return SL::JSON::decode_json($data_json);
305 my %return = ( success => 0,
306 data => { version => $url . ": " . $status_line, revision => $type },
307 message => "Server not found or wrong data type",
314 my ($self, $shop_part, $todo) = @_;
316 #shop_part is passed as a param
317 die unless ref($shop_part) eq 'SL::DB::ShopPart';
319 my $url = $self->url;
320 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
323 my $cvars = { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $part->cvars_by_config } };
326 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
327 my $temp = { ( id => @{$row_cat}[0], ) };
328 push ( @cat, $temp );
331 my @upload_img = $shop_part->get_images;
332 my $tax_n_price = $shop_part->get_tax_and_price;
333 my $price = $tax_n_price->{price};
334 my $taxrate = $tax_n_price->{tax};
335 # mapping to shopware still missing attributes,metatags
338 if($todo eq "price"){
339 %shop_data = ( mainDetail => { number => $part->partnumber,
340 prices => [ { from => 1,
342 customerGroupKey => 'EK',
347 }elsif($todo eq "stock"){
348 %shop_data = ( mainDetail => { number => $part->partnumber,
349 inStock => $part->onhand,
352 }elsif($todo eq "price_stock"){
353 %shop_data = ( mainDetail => { number => $part->partnumber,
354 inStock => $part->onhand,
355 prices => [ { from => 1,
357 customerGroupKey => 'EK',
362 }elsif($todo eq "active"){
363 %shop_data = ( mainDetail => { number => $part->partnumber,
365 active => ($part->partnumber == 1 ? 0 : 1),
367 }elsif($todo eq "all"){
368 # mapping to shopware still missing attributes,metatags
369 %shop_data = ( name => $part->description,
370 mainDetail => { number => $part->partnumber,
371 inStock => $part->onhand,
372 prices => [ { from => 1,
374 customerGroupKey => 'EK',
377 active => $shop_part->active,
378 #attribute => { attr1 => $cvars->{CVARNAME}->{value}, } , #HowTo handle attributes
380 supplier => 'AR', # Is needed by shopware,
381 descriptionLong => $shop_part->shop_description,
382 active => $shop_part->active,
383 images => [ @upload_img ],
384 __options_images => { replace => 1, },
385 categories => [ @cat ],
386 description => $shop_part->shop_description,
387 categories => [ @cat ],
393 my $dataString = SL::JSON::to_json(\%shop_data);
394 $dataString = encode_utf8($dataString);
398 my ($import,$data,$data_json);
399 my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
400 # Shopware RestApi sends an erroremail if configured and part not found. But it needs this info to decide if update or create a new article
401 # LWP->post = create LWP->put = update
402 $data = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
403 $data_json = $data->content;
404 $import = SL::JSON::decode_json($data_json);
405 if($import->{success}){
407 my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
408 $upload = $self->connector->put($url . "api/articles/$partnumber?useNumberAsId=true", Content => $dataString);
409 my $data_json = $upload->content;
410 $upload_content = SL::JSON::decode_json($data_json);
413 $upload = $self->connector->post($url . "api/articles/", Content => $dataString);
414 my $data_json = $upload->content;
415 $upload_content = SL::JSON::decode_json($data_json);
417 # don't know if this is needed
419 my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
420 my $imgup = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
423 return $upload_content->{success};
427 my ($self,$partnumber) = @_;
429 my $url = $self->url;
430 $partnumber = $::form->escape($partnumber);#shopware don't accept / in articlenumber
431 my $data = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
432 my $data_json = $data->content;
433 return SL::JSON::decode_json($data_json);
436 sub set_orderstatus {
437 my ($self,$order_id, $status) = @_;
438 if ($status eq "fetched") { $status = 1; }
439 if ($status eq "completed") { $status = 2; }
440 my %new_status = (orderStatusId => $status);
441 my $new_status_json = SL::JSON::to_json(\%new_status);
442 $self->connector->put($self->url . "api/orders/$order_id", Content => $new_status_json);
447 $self->url($self->config->protocol . "://" . $self->config->server . ":" . $self->config->port . $self->config->path);
452 my $ua = LWP::UserAgent->new;
454 $self->config->server . ":" . $self->config->port,
455 $self->config->realm,
456 $self->config->login => $self->config->password
471 SL::Shopconnecter::Shopware - connector for shopware 5
478 This is the connector to shopware.
479 In this file you can do the mapping to your needs.
480 see https://developers.shopware.com/developers-guide/rest-api/
481 for more information.
487 =item C<get_one_order>
489 Fetches one order specified by ordnumber
491 =item C<get_new_orders>
493 Fetches new order by parameters from shop configuration
495 =item C<import_data_to_shop_order>
497 Creates on shoporder object from json
498 Here is the mapping for the positions.
499 see https://developers.shopware.com/developers-guide/rest-api/
500 for detailed information
502 =item C<map_data_to_shoporder>
504 Here is the mapping for the order data.
505 see https://developers.shopware.com/developers-guide/rest-api/
506 for detailed information
508 =item C<get_categories>
512 Use this for test Connection
517 Here is the mapping for the article data.
518 see https://developers.shopware.com/developers-guide/rest-api/
519 for detailed information
539 Pricesrules, pricessources aren't fully implemented yet.
540 Payments aren't implemented( need to map payments from Shopware like invoice, paypal etc. to payments in kivitendo)
548 W. Hahn E<lt>wh@futureworldsearch.netE<gt>