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