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