WebshopApi: ShopConnector WooCommerce erstellt
[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 DateTime::Format::Strptime;
15 use SL::DB::File;
16 use Data::Dumper;
17 use Sort::Naturally ();
18 use SL::Helper::Flash;
19 use Encode qw(encode_utf8);
20 use SL::File;
21 use File::Slurp;
22
23 use Rose::Object::MakeMethods::Generic (
24   'scalar --get_set_init' => [ qw(connector url) ],
25 );
26
27 sub get_one_order {
28   my ($self, $ordnumber) = @_;
29
30   my $dbh       = SL::DB::client;
31   my $of        = 0;
32   my $url       = $self->url;
33   my $data      = $self->connector->get($url . "api/orders/$ordnumber?useNumberAsId=true");
34   my @errors;
35
36   my %fetched_orders;
37   if ($data->is_success && $data->content_type eq 'application/json'){
38     my $data_json = $data->content;
39     my $import    = SL::JSON::decode_json($data_json);
40     my $shoporder = $import->{data};
41     $dbh->with_transaction( sub{
42       $self->import_data_to_shop_order($import);
43       1;
44     })or do {
45       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
46     };
47
48     if(!@errors){
49       $self->set_orderstatus($import->{data}->{id}, "fetched");
50       $of++;
51     }else{
52       flash_later('error', $::locale->text('Database errors: #1', @errors));
53     }
54     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
55   } else {
56     my %error_msg  = (
57       shop_id          => $self->config->id,
58       shop_description => $self->config->description,
59       message          => "Error: $data->status_line",
60       error            => 1,
61     );
62     %fetched_orders = %error_msg;
63   }
64
65   return \%fetched_orders;
66 }
67
68 sub get_new_orders {
69   my ($self, $id) = @_;
70
71   my $url              = $self->url;
72   my $last_order_number = $self->config->last_order_number;
73   my $otf              = $self->config->orders_to_fetch;
74   my $of               = 0;
75   my $last_data      = $self->connector->get($url . "api/orders/$last_order_number?useNumberAsId=true");
76   my $last_data_json = $last_data->content;
77   my $last_import    = SL::JSON::decode_json($last_data_json);
78
79   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});
80
81   my $dbh = SL::DB->client;
82   my @errors;
83   my %fetched_orders;
84   if ($orders_data->is_success && $orders_data->content_type eq 'application/json'){
85     my $orders_data_json = $orders_data->content;
86     my $orders_import    = SL::JSON::decode_json($orders_data_json);
87     foreach my $shoporder(@{ $orders_import->{data} }){
88
89       my $data      = $self->connector->get($url . "api/orders/" . $shoporder->{id});
90       my $data_json = $data->content;
91       my $import    = SL::JSON::decode_json($data_json);
92
93       $dbh->with_transaction( sub{
94           $self->import_data_to_shop_order($import);
95
96           $self->config->assign_attributes( last_order_number => $shoporder->{number});
97           $self->config->save;
98           1;
99       })or do {
100         push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
101       };
102
103       if(!@errors){
104         $self->set_orderstatus($shoporder->{id}, "fetched");
105         $of++;
106       }else{
107         flash_later('error', $::locale->text('Database errors: #1', @errors));
108       }
109     }
110     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $of);
111   } else {
112     my %error_msg  = (
113       shop_id          => $self->config->id,
114       shop_description => $self->config->description,
115       message          => "Error: $orders_data->status_line",
116       error            => 1,
117     );
118     %fetched_orders = %error_msg;
119   }
120
121   return \%fetched_orders;
122 }
123
124 sub import_data_to_shop_order {
125   my ( $self, $import ) = @_;
126   my $shop_order = $self->map_data_to_shoporder($import);
127
128   $shop_order->save;
129   my $id = $shop_order->id;
130
131   my @positions = sort { Sort::Naturally::ncmp($a->{"articleNumber"}, $b->{"articleNumber"}) } @{ $import->{data}->{details} };
132   #my @positions = sort { Sort::Naturally::ncmp($a->{"partnumber"}, $b->{"partnumber"}) } @{ $import->{data}->{details} };
133   my $position = 1;
134   my $active_price_source = $self->config->price_source;
135   #Mapping Positions
136   foreach my $pos(@positions) {
137     my $price = $::form->round_amount($pos->{price},2);
138     my %pos_columns = ( description          => $pos->{articleName},
139                         partnumber           => $pos->{articleNumber},
140                         price                => $price,
141                         quantity             => $pos->{quantity},
142                         position             => $position,
143                         tax_rate             => $pos->{taxRate},
144                         shop_trans_id        => $pos->{articleId},
145                         shop_order_id        => $id,
146                         active_price_source  => $active_price_source,
147                       );
148     my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
149     $pos_insert->save;
150     $position++;
151   }
152   $shop_order->positions($position-1);
153
154   if ( $self->config->shipping_costs_parts_id ) {
155     my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
156     my %shipping_pos = ( description    => $import->{data}->{dispatch}->{name},
157                          partnumber     => $shipping_part->partnumber,
158                          price          => $import->{data}->{invoiceShipping},
159                          quantity       => 1,
160                          position       => $position,
161                          shop_trans_id  => 0,
162                          shop_order_id  => $id,
163                        );
164     my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
165     $shipping_pos_insert->save;
166   }
167
168   my $customer = $shop_order->get_customer;
169
170   if(ref($customer)){
171     $shop_order->kivi_customer_id($customer->id);
172   }
173   $shop_order->save;
174 }
175
176 sub map_data_to_shoporder {
177   my ($self, $import) = @_;
178
179   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
180                                                   locale    => 'de_DE',
181                                                   time_zone => 'local'
182                                                 );
183   my $orderdate = $parser->parse_datetime($import->{data}->{orderTime});
184
185   my $shop_id      = $self->config->id;
186   my $tax_included = $self->config->pricetype;
187
188   # Mapping to table shoporders. See http://community.shopware.com/_detail_1690.html#GET_.28Liste.29
189   my %columns = (
190     amount                  => $import->{data}->{invoiceAmount},
191     billing_city            => $import->{data}->{billing}->{city},
192     billing_company         => $import->{data}->{billing}->{company},
193     billing_country         => $import->{data}->{billing}->{country}->{name},
194     billing_department      => $import->{data}->{billing}->{department},
195     billing_email           => $import->{data}->{customer}->{email},
196     billing_fax             => $import->{data}->{billing}->{fax},
197     billing_firstname       => $import->{data}->{billing}->{firstName},
198     #billing_greeting        => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
199     billing_lastname        => $import->{data}->{billing}->{lastName},
200     billing_phone           => $import->{data}->{billing}->{phone},
201     billing_street          => $import->{data}->{billing}->{street},
202     billing_vat             => $import->{data}->{billing}->{vatId},
203     billing_zipcode         => $import->{data}->{billing}->{zipCode},
204     customer_city           => $import->{data}->{billing}->{city},
205     customer_company        => $import->{data}->{billing}->{company},
206     customer_country        => $import->{data}->{billing}->{country}->{name},
207     customer_department     => $import->{data}->{billing}->{department},
208     customer_email          => $import->{data}->{customer}->{email},
209     customer_fax            => $import->{data}->{billing}->{fax},
210     customer_firstname      => $import->{data}->{billing}->{firstName},
211     #customer_greeting       => ($import->{data}->{billing}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
212     customer_lastname       => $import->{data}->{billing}->{lastName},
213     customer_phone          => $import->{data}->{billing}->{phone},
214     customer_street         => $import->{data}->{billing}->{street},
215     customer_vat            => $import->{data}->{billing}->{vatId},
216     customer_zipcode        => $import->{data}->{billing}->{zipCode},
217     customer_newsletter     => $import->{data}->{customer}->{newsletter},
218     delivery_city           => $import->{data}->{shipping}->{city},
219     delivery_company        => $import->{data}->{shipping}->{company},
220     delivery_country        => $import->{data}->{shipping}->{country}->{name},
221     delivery_department     => $import->{data}->{shipping}->{department},
222     delivery_email          => "",
223     delivery_fax            => $import->{data}->{shipping}->{fax},
224     delivery_firstname      => $import->{data}->{shipping}->{firstName},
225     #delivery_greeting       => ($import->{data}->{shipping}->{salutation} eq 'mr' ? 'Herr' : 'Frau'),
226     delivery_lastname       => $import->{data}->{shipping}->{lastName},
227     delivery_phone          => $import->{data}->{shipping}->{phone},
228     delivery_street         => $import->{data}->{shipping}->{street},
229     delivery_vat            => $import->{data}->{shipping}->{vatId},
230     delivery_zipcode        => $import->{data}->{shipping}->{zipCode},
231     host                    => $import->{data}->{shop}->{hosts},
232     netamount               => $import->{data}->{invoiceAmountNet},
233     order_date              => $orderdate,
234     payment_description     => $import->{data}->{payment}->{description},
235     payment_id              => $import->{data}->{paymentId},
236     remote_ip               => $import->{data}->{remoteAddress},
237     sepa_account_holder     => $import->{data}->{paymentIntances}->{accountHolder},
238     sepa_bic                => $import->{data}->{paymentIntances}->{bic},
239     sepa_iban               => $import->{data}->{paymentIntances}->{iban},
240     shipping_costs          => $import->{data}->{invoiceShipping},
241     shipping_costs_net      => $import->{data}->{invoiceShippingNet},
242     shop_c_billing_id       => $import->{data}->{billing}->{customerId},
243     shop_c_billing_number   => $import->{data}->{billing}->{number},
244     shop_c_delivery_id      => $import->{data}->{shipping}->{id},
245     shop_customer_id        => $import->{data}->{customerId},
246     shop_customer_number    => $import->{data}->{billing}->{number},
247     shop_customer_comment   => $import->{data}->{customerComment},
248     shop_id                 => $shop_id,
249     shop_ordernumber        => $import->{data}->{number},
250     shop_trans_id           => $import->{data}->{id},
251     tax_included            => $tax_included eq "brutto" ? 1 : 0,
252   );
253
254   my $shop_order = SL::DB::ShopOrder->new(%columns);
255   return $shop_order;
256 }
257
258 sub get_categories {
259   my ($self) = @_;
260
261   my $url        = $self->url;
262   my $data       = $self->connector->get($url . "api/categories");
263   my $data_json  = $data->content;
264   my $import     = SL::JSON::decode_json($data_json);
265   my @daten      = @{$import->{data}};
266   my %categories = map { ($_->{id} => $_) } @daten;
267
268   my @categories_tree;
269   for(@daten) {
270     # ignore root with id=1
271     if( $_->{id} == 1) {
272       next;
273     }
274     my $parent = $categories{$_->{parentId}};
275     if($parent && $parent->{id} != 1) {
276       $parent->{children} ||= [];
277       push @{$parent->{children}},$_;
278     } else {
279       push @categories_tree, $_;
280     }
281   }
282
283   return \@categories_tree;
284 }
285
286 sub get_version {
287   my ($self) = @_;
288
289   my $url       = $self->url;
290   my $data      = $self->connector->get($url . "api/version");
291   my $type = $data->content_type;
292   my $status_line = $data->status_line;
293
294   if($data->is_success && $type eq 'application/json'){
295     my $data_json = $data->content;
296     return SL::JSON::decode_json($data_json);
297   }else{
298     my %return = ( success => 0,
299                    data    => { version => $url . ": " . $status_line, revision => $type },
300                    message => "Server not found or wrong data type",
301                 );
302     return \%return;
303   }
304 }
305
306 sub update_part {
307   my ($self, $shop_part, $todo) = @_;
308
309   #shop_part is passed as a param
310   die unless ref($shop_part) eq 'SL::DB::ShopPart';
311
312   my $url = $self->url;
313   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
314
315   # CVARS to map
316   my $cvars = { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $part->cvars_by_config } };
317
318   my @cat = ();
319   foreach my $row_cat ( @{ $shop_part->shop_category } ) {
320     my $temp = { ( id => @{$row_cat}[0], ) };
321     push ( @cat, $temp );
322   }
323
324   my @upload_img = $shop_part->get_images;
325   my $tax_n_price = $shop_part->get_tax_and_price;
326   my $price = $tax_n_price->{price};
327   my $taxrate = $tax_n_price->{tax};
328   # mapping to shopware still missing attributes,metatags
329   my %shop_data;
330
331   if($todo eq "price"){
332     %shop_data = ( mainDetail => { number   => $part->partnumber,
333                                    prices   =>  [ { from             => 1,
334                                                     price            => $price,
335                                                     customerGroupKey => 'EK',
336                                                   },
337                                                 ],
338                                   },
339                  );
340   }elsif($todo eq "stock"){
341     %shop_data = ( mainDetail => { number   => $part->partnumber,
342                                    inStock  => $part->onhand,
343                                  },
344                  );
345   }elsif($todo eq "price_stock"){
346     %shop_data =  ( mainDetail => { number   => $part->partnumber,
347                                     inStock  => $part->onhand,
348                                     prices   =>  [ { from             => 1,
349                                                      price            => $price,
350                                                      customerGroupKey => 'EK',
351                                                    },
352                                                  ],
353                                    },
354                    );
355   }elsif($todo eq "active"){
356     %shop_data =  ( mainDetail => { number   => $part->partnumber,
357                                    },
358                     active => ($part->partnumber == 1 ? 0 : 1),
359                    );
360   }elsif($todo eq "all"){
361   # mapping to shopware still missing attributes,metatags
362     %shop_data =  (   name              => $part->description,
363                       mainDetail        => { number   => $part->partnumber,
364                                              inStock  => $part->onhand,
365                                              prices   =>  [ {          from   => 1,
366                                                                        price  => $price,
367                                                             customerGroupKey  => 'EK',
368                                                             },
369                                                           ],
370                                              active   => $shop_part->active,
371                                              #attribute => { attr1  => $cvars->{CVARNAME}->{value}, } , #HowTo handle attributes
372                                        },
373                       supplier          => 'AR', # Is needed by shopware,
374                       descriptionLong   => $shop_part->shop_description,
375                       active            => $shop_part->active,
376                       images            => [ @upload_img ],
377                       __options_images  => { replace => 1, },
378                       categories        => [ @cat ],
379                       description       => $shop_part->shop_description,
380                       categories        => [ @cat ],
381                       tax               => $taxrate,
382                     )
383                   ;
384   }
385
386   my $dataString = SL::JSON::to_json(\%shop_data);
387   $dataString    = encode_utf8($dataString);
388
389   my $upload_content;
390   my $upload;
391   my ($import,$data,$data_json);
392   my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
393   # 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
394   # LWP->post = create LWP->put = update
395     $data       = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
396     $data_json  = $data->content;
397     $import     = SL::JSON::decode_json($data_json);
398   if($import->{success}){
399     #update
400     my $partnumber  = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
401     $upload         = $self->connector->put($url . "api/articles/$partnumber?useNumberAsId=true", Content => $dataString);
402     my $data_json   = $upload->content;
403     $upload_content = SL::JSON::decode_json($data_json);
404   }else{
405     #upload
406     $upload         = $self->connector->post($url . "api/articles/", Content => $dataString);
407     my $data_json   = $upload->content;
408     $upload_content = SL::JSON::decode_json($data_json);
409   }
410   # don't know if this is needed
411   if(@upload_img) {
412     my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
413     my $imgup      = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
414   }
415
416   return $upload_content->{success};
417 }
418
419 sub get_article {
420   my ($self,$partnumber) = @_;
421
422   my $url       = $self->url;
423   $partnumber   = $::form->escape($partnumber);#shopware don't accept / in articlenumber
424   my $data      = $self->connector->get($url . "api/articles/$partnumber?useNumberAsId=true");
425   my $data_json = $data->content;
426   return SL::JSON::decode_json($data_json);
427 }
428
429 sub set_orderstatus {
430   my ($self,$order_id, $status) = @_;
431   if ($status eq "fetched") { $status = 1; }
432   if ($status eq "completed") { $status = 2; }
433   my %new_status = (orderStatusId => $status);
434   my $new_status_json = SL::JSON::to_json(\%new_status);
435   $self->connector->put($self->url . "api/orders/$order_id", Content => $new_status_json);
436 }
437
438 sub init_url {
439   my ($self) = @_;
440   $self->url($self->config->protocol . "://" . $self->config->server . ":" . $self->config->port . $self->config->path);
441 }
442
443 sub init_connector {
444   my ($self) = @_;
445   my $ua = LWP::UserAgent->new;
446   $ua->credentials(
447       $self->config->server . ":" . $self->config->port,
448       $self->config->realm,
449       $self->config->login => $self->config->password
450   );
451
452   return $ua;
453
454 }
455
456 1;
457
458 __END__
459
460 =encoding utf-8
461
462 =head1 NAME
463
464 SL::Shopconnecter::Shopware - connector for shopware 5
465
466 =head1 SYNOPSIS
467
468
469 =head1 DESCRIPTION
470
471 This is the connector to shopware.
472 In this file you can do the mapping to your needs.
473 see https://developers.shopware.com/developers-guide/rest-api/
474 for more information.
475
476 =head1 METHODS
477
478 =over 4
479
480 =item C<get_one_order>
481
482 Fetches one order specified by ordnumber
483
484 =item C<get_new_orders>
485
486 Fetches new order by parameters from shop configuration
487
488 =item C<import_data_to_shop_order>
489
490 Creates on shoporder object from json
491 Here is the mapping for the positions.
492 see https://developers.shopware.com/developers-guide/rest-api/
493 for detailed information
494
495 =item C<map_data_to_shoporder>
496
497 Here is the mapping for the order data.
498 see https://developers.shopware.com/developers-guide/rest-api/
499 for detailed information
500
501 =item C<get_categories>
502
503 =item C<get_version>
504
505 Use this for test Connection
506 see SL::Shop
507
508 =item C<update_part>
509
510 Here is the mapping for the article data.
511 see https://developers.shopware.com/developers-guide/rest-api/
512 for detailed information
513
514 =item C<get_article>
515
516 =back
517
518 =head1 INITS
519
520 =over 4
521
522 =item init_url
523
524 build an url for LWP
525
526 =item init_connector
527
528 =back
529
530 =head1 TODO
531
532 Pricesrules, pricessources aren't fully implemented yet.
533 Payments aren't implemented( need to map payments from Shopware like invoice, paypal etc. to payments in kivitendo)
534
535 =head1 BUGS
536
537 None yet. :)
538
539 =head1 AUTHOR
540
541 W. Hahn E<lt>wh@futureworldsearch.netE<gt>
542
543 =cut