Merge branch 'test' of ../kivitendo-erp_20220811
[kivitendo-erp.git] / SL / ShopConnector / Shopware.pm
1 package SL::ShopConnector::Shopware;
2
3 use strict;
4
5 use parent qw(SL::ShopConnector::Base);
6
7
8 use SL::JSON;
9 use LWP::UserAgent;
10 use LWP::Authen::Digest;
11 use SL::DB::ShopOrder;
12 use SL::DB::ShopOrderItem;
13 use SL::DB::History;
14 use SL::DB::PaymentTerm;
15 use DateTime::Format::Strptime;
16 use SL::DB::File;
17 use Data::Dumper;
18 use Sort::Naturally ();
19 use SL::Helper::Flash;
20 use Encode qw(encode_utf8);
21 use SL::File;
22 use File::Slurp;
23
24 use Rose::Object::MakeMethods::Generic (
25   'scalar --get_set_init' => [ qw(connector url) ],
26 );
27
28 sub get_one_order {
29   my ($self, $ordnumber) = @_;
30
31   my $dbh       = SL::DB::client;
32   my $of        = 0;
33   my $url       = $self->url;
34   my $data      = $self->connector->get($url . "api/orders/$ordnumber?useNumberAsId=true");
35   my @errors;
36
37   my %fetched_orders;
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);
44       1;
45     })or do {
46       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
47     };
48
49     if(!@errors){
50       $self->set_orderstatus($import->{data}->{id}, "fetched");
51       $of++;
52     }else{
53       flash_later('error', $::locale->text('Database errors: #1', @errors));
54     }
55     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
56   } else {
57     my %error_msg  = (
58       shop_id          => $self->config->id,
59       shop_description => $self->config->description,
60       message          => "Error: $data->status_line",
61       error            => 1,
62     );
63     %fetched_orders = %error_msg;
64   }
65
66   return \%fetched_orders;
67 }
68
69 sub get_new_orders {
70   my ($self, $id) = @_;
71
72   my $url              = $self->url;
73   my $last_order_number = $self->config->last_order_number;
74   my $otf              = $self->config->orders_to_fetch;
75   my $of               = 0;
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);
79
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});
81
82   my $dbh = SL::DB->client;
83   my @errors;
84   my %fetched_orders;
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} }){
89
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);
93
94       $dbh->with_transaction( sub{
95           $self->import_data_to_shop_order($import);
96
97           $self->config->assign_attributes( last_order_number => $shoporder->{number});
98           $self->config->save;
99           1;
100       })or do {
101         push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
102       };
103
104       if(!@errors){
105         $self->set_orderstatus($shoporder->{id}, "fetched");
106         $of++;
107       }else{
108         flash_later('error', $::locale->text('Database errors: #1', @errors));
109       }
110     }
111     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
112   } else {
113     my %error_msg  = (
114       shop_id          => $self->config->id,
115       shop_description => $self->config->description,
116       message          => "Error: $orders_data->status_line",
117       error            => 1,
118     );
119     %fetched_orders = %error_msg;
120   }
121
122   return \%fetched_orders;
123 }
124
125 sub import_data_to_shop_order {
126   my ( $self, $import ) = @_;
127   my $shop_order = $self->map_data_to_shoporder($import);
128
129   $shop_order->save;
130   my $id = $shop_order->id;
131
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} };
134   my $position = 1;
135   my $active_price_source = $self->config->price_source;
136   #Mapping Positions
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},
141                         price                => $price,
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,
148                       );
149     my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
150     $pos_insert->save;
151     $position++;
152   }
153   $shop_order->positions($position-1);
154
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},
160                          quantity       => 1,
161                          position       => $position,
162                          shop_trans_id  => 0,
163                          shop_order_id  => $id,
164                        );
165     my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
166     $shipping_pos_insert->save;
167   }
168
169   my $customer = $shop_order->get_customer;
170
171   if(ref($customer)){
172     $shop_order->kivi_customer_id($customer->id);
173   }
174   $shop_order->save;
175 }
176
177 sub map_data_to_shoporder {
178   my ($self, $import) = @_;
179
180   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
181                                                   locale    => 'de_DE',
182                                                   time_zone => 'local'
183                                                 );
184   my $orderdate = $parser->parse_datetime($import->{data}->{orderTime});
185
186   my $shop_id      = $self->config->id;
187   my $tax_included = $self->config->pricetype;
188
189   # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
190   my %payment_ids_methods = (
191     # shopware_paymentId => kivitendo_payment_id
192   );
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
196   my %columns = (
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},
255     shop_id                 => $shop_id,
256     shop_ordernumber        => $import->{data}->{number},
257     shop_trans_id           => $import->{data}->{id},
258     tax_included            => $tax_included eq "brutto" ? 1 : 0,
259   );
260
261   my $shop_order = SL::DB::ShopOrder->new(%columns);
262   return $shop_order;
263 }
264
265 sub get_categories {
266   my ($self) = @_;
267
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;
274
275   my @categories_tree;
276   for(@daten) {
277     # ignore root with id=1
278     if( $_->{id} == 1) {
279       next;
280     }
281     my $parent = $categories{$_->{parentId}};
282     if($parent && $parent->{id} != 1) {
283       $parent->{children} ||= [];
284       push @{$parent->{children}},$_;
285     } else {
286       push @categories_tree, $_;
287     }
288   }
289
290   return \@categories_tree;
291 }
292
293 sub get_version {
294   my ($self) = @_;
295
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;
300
301   if($data->is_success && $type eq 'application/json'){
302     my $data_json = $data->content;
303     return SL::JSON::decode_json($data_json);
304   }else{
305     my %return = ( success => 0,
306                    data    => { version => $url . ": " . $status_line, revision => $type },
307                    message => "Server not found or wrong data type",
308                 );
309     return \%return;
310   }
311 }
312
313 sub update_part {
314   my ($self, $shop_part, $todo) = @_;
315
316   #shop_part is passed as a param
317   die unless ref($shop_part) eq 'SL::DB::ShopPart';
318
319   my $url = $self->url;
320   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
321
322   # CVARS to map
323   my $cvars = { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $part->cvars_by_config } };
324
325   my @cat = ();
326   foreach my $row_cat ( @{ $shop_part->shop_category } ) {
327     my $temp = { ( id => @{$row_cat}[0], ) };
328     push ( @cat, $temp );
329   }
330
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
336   my %shop_data;
337
338   if($todo eq "price"){
339     %shop_data = ( mainDetail => { number   => $part->partnumber,
340                                    prices   =>  [ { from             => 1,
341                                                     price            => $price,
342                                                     customerGroupKey => 'EK',
343                                                   },
344                                                 ],
345                                   },
346                  );
347   }elsif($todo eq "stock"){
348     %shop_data = ( mainDetail => { number   => $part->partnumber,
349                                    inStock  => $part->onhand,
350                                  },
351                  );
352   }elsif($todo eq "price_stock"){
353     %shop_data =  ( mainDetail => { number   => $part->partnumber,
354                                     inStock  => $part->onhand,
355                                     prices   =>  [ { from             => 1,
356                                                      price            => $price,
357                                                      customerGroupKey => 'EK',
358                                                    },
359                                                  ],
360                                    },
361                    );
362   }elsif($todo eq "active"){
363     %shop_data =  ( mainDetail => { number   => $part->partnumber,
364                                    },
365                     active => ($part->partnumber == 1 ? 0 : 1),
366                    );
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,
373                                                                        price  => $price,
374                                                             customerGroupKey  => 'EK',
375                                                             },
376                                                           ],
377                                              active   => $shop_part->active,
378                                              #attribute => { attr1  => $cvars->{CVARNAME}->{value}, } , #HowTo handle attributes
379                                        },
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 ],
388                       tax               => $taxrate,
389                     )
390                   ;
391   }
392
393   my $dataString = SL::JSON::to_json(\%shop_data);
394   $dataString    = encode_utf8($dataString);
395
396   my $upload_content;
397   my $upload;
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}){
406     #update
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);
411   }else{
412     #upload
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);
416   }
417   # don't know if this is needed
418   if(@upload_img) {
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");
421   }
422
423   return $upload_content->{success};
424 }
425
426 sub get_article {
427   my ($self,$partnumber) = @_;
428
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);
434 }
435
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);
443 }
444
445 sub init_url {
446   my ($self) = @_;
447   $self->url($self->config->protocol . "://" . $self->config->server . ":" . $self->config->port . $self->config->path);
448 }
449
450 sub init_connector {
451   my ($self) = @_;
452   my $ua = LWP::UserAgent->new;
453   $ua->credentials(
454       $self->config->server . ":" . $self->config->port,
455       $self->config->realm,
456       $self->config->login => $self->config->password
457   );
458
459   return $ua;
460
461 }
462
463 1;
464
465 __END__
466
467 =encoding utf-8
468
469 =head1 NAME
470
471 SL::Shopconnector::Shopware - connector for shopware 5
472
473 =head1 SYNOPSIS
474
475
476 =head1 DESCRIPTION
477
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.
482
483 =head1 METHODS
484
485 =over 4
486
487 =item C<get_one_order>
488
489 Fetches one order specified by ordnumber
490
491 =item C<get_new_orders>
492
493 Fetches new order by parameters from shop configuration
494
495 =item C<import_data_to_shop_order>
496
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
501
502 =item C<map_data_to_shoporder>
503
504 Here is the mapping for the order data.
505 see https://developers.shopware.com/developers-guide/rest-api/
506 for detailed information
507
508 =item C<get_categories>
509
510 =item C<get_version>
511
512 Use this for test Connection
513 see SL::Shop
514
515 =item C<update_part>
516
517 Here is the mapping for the article data.
518 see https://developers.shopware.com/developers-guide/rest-api/
519 for detailed information
520
521 =item C<get_article>
522
523 =back
524
525 =head1 INITS
526
527 =over 4
528
529 =item init_url
530
531 build an url for LWP
532
533 =item init_connector
534
535 =back
536
537 =head1 TODO
538
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)
541
542 =head1 BUGS
543
544 None yet. :)
545
546 =head1 AUTHOR
547
548 W. Hahn E<lt>wh@futureworldsearch.netE<gt>
549
550 =cut