9a3f8eafbf42b8498a6f2ec10ba995e71ee108b2
[kivitendo-erp.git] / SL / ShopConnector / WooCommerce.pm
1 package SL::ShopConnector::WooCommerce;
2
3 use strict;
4
5 use parent qw(SL::ShopConnector::Base);
6
7 use SL::JSON;
8 use LWP::UserAgent;
9 use LWP::Authen::Digest;
10 use SL::DB::ShopOrder;
11 use SL::DB::ShopOrderItem;
12 use SL::DB::History;
13 use SL::DB::File;
14 use Data::Dumper;
15 use SL::Helper::Flash;
16 use Encode qw(encode_utf8);
17
18 sub get_one_order {
19   my ($self, $order_id) = @_;
20
21   my $dbh       = SL::DB::client;
22   my $number_of_orders = 0;
23   my @errors;
24
25   my $answer = $self->send_request(
26     "orders/" . $order_id,
27     undef,
28     "get"
29   );
30   my %fetched_orders;
31   if($answer->{success}) {
32     my $shoporder = $answer->{data};
33
34     $dbh->with_transaction( sub{
35         unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
36
37         #update status on server
38         $shoporder->{status} = "processing";
39         my %new_status = ( status => "processing" );
40         my $status_json = SL::JSON::to_json( \%new_status);
41         $answer = $self->send_request("orders/$shoporder->{id}", $status_json, "put");
42         unless($answer->{success}){
43           push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
44           return 0
45         }
46
47         1;
48       })or do {
49       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
50     };
51
52     if(@errors){
53       flash_later('error', $::locale->text('Errors: #1', @errors));
54     } else {
55       $number_of_orders++;
56     }
57     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
58   } else {
59     my %error_msg  = (
60       shop_id          => $self->config->id,
61       shop_description => $self->config->description,
62       message          => $answer->{message},
63       error            => 1,
64     );
65     %fetched_orders = %error_msg;
66   }
67   return \%fetched_orders;
68 }
69
70 sub get_new_orders {
71   my ($self) = @_;
72
73   my $dbh       = SL::DB::client;
74   my $otf              = $self->config->orders_to_fetch || 10;
75   my $number_of_orders = 0;
76   my @errors;
77
78   my $answer = $self->send_request(
79     "orders",
80     undef,
81     "get",
82     "&per_page=$otf&status=pending"
83   );
84   my %fetched_orders;
85   if($answer->{success}) {
86     my $orders = $answer->{data};
87     foreach my $shoporder(@{$orders}){
88
89       $dbh->with_transaction( sub{
90           unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
91
92           #update status on server
93           $shoporder->{status} = "processing";
94           my %new_status = ( status => "processing" );
95           my $status_json = SL::JSON::to_json( \%new_status);
96           $answer = $self->send_request("orders/$shoporder->{id}", $status_json, "put");
97           unless($answer->{success}){
98             push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
99             return 0;
100           }
101
102           1;
103       })or do {
104         push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
105       };
106
107       if(@errors){
108         flash_later('error', $::locale->text('Errors: #1', @errors));
109       } else {
110         $number_of_orders++;
111       }
112     }
113     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
114
115   } else {
116     my %error_msg  = (
117       shop_id          => $self->config->id,
118       shop_description => $self->config->description,
119       message          => $answer->{message},
120       error            => 1,
121     );
122     %fetched_orders = %error_msg;
123   }
124
125   return \%fetched_orders;
126 }
127
128 sub import_data_to_shop_order {
129   my ( $self, $import ) = @_;
130   my $shop_order = $self->map_data_to_shoporder($import);
131
132   $shop_order->save;
133   my $id = $shop_order->id;
134
135   my @positions = sort { Sort::Naturally::ncmp($a->{"sku"}, $b->{"sku"}) } @{ $import->{line_items} };
136   my $position = 1;
137
138   my $answer= $self->send_request("taxes");
139   unless ($answer->{success}){ return 0;}
140   my %taxes = map { ($_->{id} => $_) } @{ $answer->{data} };
141
142   my $active_price_source = $self->config->price_source;
143   #Mapping Positions
144   foreach my $pos(@positions) {
145     my $price = $::form->round_amount($pos->{total},2);
146     my $tax_id = $pos->{taxes}[0]->{id};
147     my $tax_rate = $taxes{ $tax_id }{rate};
148     my %pos_columns = ( description          => $pos->{name},
149                         partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
150                         price                => $price,
151                         quantity             => $pos->{quantity},
152                         position             => $position,
153                         tax_rate             => $tax_rate,
154                         shop_trans_id        => $pos->{product_id},
155                         shop_order_id        => $id,
156                         active_price_source  => $active_price_source,
157                       );
158     my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
159     $pos_insert->save;
160     $position++;
161   }
162   $shop_order->positions($position-1);
163
164   my $customer = $shop_order->get_customer;
165
166   if(ref($customer)){
167     $shop_order->kivi_customer_id($customer->id);
168   }
169   $shop_order->save;
170 }
171
172
173 sub map_data_to_shoporder {
174   my ($self, $import) = @_;
175
176   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
177                                                   locale    => 'de_DE',
178                                                   time_zone => 'local'
179                                                 );
180
181   my $shop_id      = $self->config->id;
182
183   # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
184   my %columns = (
185 #billing
186     billing_firstname       => $import->{billing}->{first_name},
187     billing_lastname        => $import->{billing}->{last_name},
188     #address_1 address_2
189     billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
190     # ???
191     billing_city            => $import->{billing}->{city},
192     #state
193     # ???
194     billing_zipcode         => $import->{billing}->{postcode},
195     billing_country         => $import->{billing}->{country},
196     billing_email           => $import->{billing}->{email},
197     billing_phone           => $import->{billing}->{phone},
198
199     #billing_greeting        => "",
200     #billing_fax             => "",
201     #billing_vat             => "",
202     #billing_company         => "",
203     #billing_department      => "",
204
205 #customer
206     #customer_id
207     shop_customer_id        => $import->{customer_id},
208     shop_customer_number    => $import->{customer_id},
209     #customer_ip_address
210     remote_ip               => $import->{customer_ip_address},
211     #customer_user_agent
212     #customer_note
213     shop_customer_comment   => $import->{customer_note},
214
215     #customer_city           => "",
216     #customer_company        => "",
217     #customer_country        => "",
218     #customer_department     => "",
219     #customer_email          => "",
220     #customer_fax            => "",
221     #customer_firstname      => "",
222     #customer_greeting       => "",
223     #customer_lastname       => "",
224     #customer_phone          => "",
225     #customer_street         => "",
226     #customer_vat            => "",
227
228 #shipping
229     delivery_firstname      => $import->{shipping}->{first_name},
230     delivery_lastname       => $import->{shipping}->{last_name},
231     delivery_company        => $import->{shipping}->{company},
232     #address_1 address_2
233     delivery_street         => $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : ""),
234     delivery_city           => $import->{shipping}->{city},
235     #state ???
236     delivery_zipcode        => $import->{shipping}->{postcode},
237     delivery_country        => $import->{shipping}->{country},
238     #delivery_department     => "",
239     #delivery_email          => "",
240     #delivery_fax            => "",
241     #delivery_phone          => "",
242     #delivery_vat            => "",
243
244 #other
245     #id
246     #parent_id
247     #number
248     shop_ordernumber        => $import->{number},
249     #order_key
250     #created_via
251     #version
252     #status
253     #currency
254     #date_created
255     order_date              => $parser->parse_datetime($import->{date_created}),
256     #date_created_gmt
257     #date_modified
258     #date_modified_gmt
259     #discount_total
260     #discount_tax
261     #shipping_total
262     shipping_costs          => $import->{shipping_costs},
263     #shipping_tax
264     shipping_costs_net      => $import->{shipping_costs} - $import->{shipping_tax},
265     #cart_tax
266     #total
267     amount                  => $import->{total},
268     #total_tax
269     netamount               => $import->{total} - $import->{total_tax},
270     #prices_include_tax
271     tax_included            => $import->{prices_include_tax} eq "true" ? 1 : 0,
272     #payment_method
273     # ??? payment_id              => $import->{payment_method},
274     #payment_method_title
275     payment_description     => $import->{payment}->{payment_method_title},
276     #transaction_id
277     shop_trans_id           => $import->{id},
278     #date_paid
279     #date_paid_gmt
280     #date_completed
281     #date_completed_gmt
282
283     host                    => $import->{_links}->{self}[0]->{href},
284
285     #sepa_account_holder     => "",
286     #sepa_bic                => "",
287     #sepa_iban               => "",
288
289     #shop_c_billing_id       => "",
290     #shop_c_billing_number   => "",
291     shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
292
293 # not in Shop
294     shop_id                 => $shop_id,
295   );
296
297   my $shop_order = SL::DB::ShopOrder->new(%columns);
298   return $shop_order;
299 }
300
301 #TODO CVARS, tax and images
302 sub update_part {
303   my ($self, $shop_part, $todo) = @_;
304
305   #shop_part is passed as a param
306   die unless ref($shop_part) eq 'SL::DB::ShopPart';
307   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
308
309   # CVARS to map
310   #my $cvars = {
311   #  map {
312   #    ($_->config->name => {
313   #      value => $_->value_as_text,
314   #      is_valid => $_->is_valid
315   #    })
316   #  }
317   #  @{ $part->cvars_by_config }
318   #};
319
320   my @categories = ();
321   foreach my $row_cat ( @{ $shop_part->shop_category } ) {
322     my $temp = { ( id => @{$row_cat}[0], ) };
323     push ( @categories, $temp );
324   }
325
326   #my @upload_img = $shop_part->get_images;
327   my $partnumber = $::form->escape($part->partnumber);#don't accept / in articlenumber
328   my $stock_status = ($part->onhand ? "instock" : "outofstock");
329   my $status = ($shop_part->active ? "publish" : "private");
330   my $tax_n_price = $shop_part->get_tax_and_price;
331   my $price = $tax_n_price->{price};
332   #my $taxrate = $tax_n_price->{tax};
333   #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
334
335   my %shop_data;
336
337   if($todo eq "price"){
338     %shop_data = (
339       regular_price => $price,
340     );
341   }elsif($todo eq "stock"){
342     %shop_data = (
343       stock_status => $stock_status,
344     );
345   }elsif($todo eq "price_stock"){
346     %shop_data =  (
347       stock_status => $stock_status,
348       regular_price => $price,
349     );
350   }elsif($todo eq "active"){
351     %shop_data =  (
352       status => $status,
353     );
354   }elsif($todo eq "all"){
355   # mapping  still missing attributes,metatags
356     %shop_data =  (
357       sku => $partnumber,
358       name => $part->description,
359       stock_status => $stock_status,
360       regular_price => $price,
361       status => $status,
362       description=> $shop_part->shop_description,
363       short_description=> $shop_part->shop_description,
364       categories => [ @categories ],
365       #tax_class => $tax_class,
366     );
367   }
368
369   my $dataString = SL::JSON::to_json(\%shop_data);
370   $dataString    = encode_utf8($dataString);
371
372   # LWP->post = create || LWP->put = update
373   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
374
375   if($answer->{success} && scalar @{$answer->{data}}){
376     #update
377     my $woo_shop_part_id = $answer->{data}[0]->{id};
378     $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
379   }else{
380     #upload
381     $answer = $self->send_request("products", $dataString, "post");
382   }
383
384   # don't know if this is needed
385   #if(@upload_img) {
386   #  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
387   #  my $imgup      = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
388   #}
389
390   return $answer->{success};
391 }
392
393 sub get_article {
394   my ($self) = @_;
395   my $partnumber = $_[1];
396
397   $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
398   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
399
400   if($answer->{success} && scalar @{$answer->{data}}){
401     my $article = $answer->{data}[0];
402     return $article;
403   } else {
404     #What shut be here?
405     return $answer
406   }
407 }
408
409 sub get_categories {
410   my ($self) = @_;
411
412   my $answer = $self->send_request("products/categories");
413   unless($answer->{success}) {
414     return $answer;
415   }
416   my @data = @{$answer->{data}};
417   my %categories = map { ($_->{id} => $_) } @data;
418
419   my @categories_tree;
420   for(@data) {
421     my $parent = $categories{$_->{parent}};
422     if($parent) {
423       $parent->{children} ||= [];
424       push @{$parent->{children}},$_;
425     } else {
426       push @categories_tree, $_;
427     }
428   }
429
430   return \@categories_tree;
431 }
432
433 sub get_version {
434   my ($self) = @_;
435
436   my $answer = $self->send_request("system_status");
437   if($answer->{success}) {
438     my $version = $answer->{data}->{environment}->{version};
439     my %return = (
440       success => 1,
441       data    => { version => $version },
442     );
443     return \%return;
444   } else {
445     return $answer;
446   }
447 }
448
449 sub create_url {
450   my ($self) = @_;
451   my $request = $_[1];
452   my $parameters = $_[2];
453
454   my $consumer_key = $self->config->login;
455   my $consumer_secret = $self->config->password;
456   my $protocol = $self->config->protocol;
457   my $server = $self->config->server;
458   my $port = $self->config->port;
459   my $path = $self->config->path;
460
461   return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
462 }
463
464 sub send_request {
465   my ($self) = @_;
466   my $request = $_[1];
467   my $json_data = $_[2];
468   my $method_type = $_[3];
469   my $parameters = $_[4];
470
471   my $ua = LWP::UserAgent->new;
472   my $url = $self->create_url( $request, $parameters );
473
474   my $answer;
475   if( $method_type eq "put" ) {
476     $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
477   } elsif ( $method_type eq "post") {
478     $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
479   } else {
480     $answer = $ua->get($url);
481   }
482
483   my $type = $answer->content_type;
484   my $status_line = $answer->status_line;
485
486   my %return;
487   if($answer->is_success && $type eq 'application/json'){
488     my $data_json = $answer->content;
489     my $json = SL::JSON::decode_json($data_json);
490     %return = (
491       success => 1,
492       data    => $json,
493     );
494   }else{
495     %return = (
496       success => 0,
497       data    => { version => $url . ": " . $status_line, data_type => $type },
498       message => "Error: $status_line",
499     );
500   }
501   #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);
502   return \%return;
503
504 }
505
506 1;