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