3ce4f4186089373e666b29ccd0fba63f3f1d1fe7
[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         #update status on server
36         $shoporder->{status} = "processing";
37         my $answer = $self->set_orderstatus($shoporder->{id}, "fetched");
38         unless($answer){
39           push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
40           return 0;
41         }
42
43         unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
44
45         1;
46       })or do {
47       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
48     };
49
50     if(@errors){
51       flash_later('error', $::locale->text('Errors: #1', @errors));
52     } else {
53       $number_of_orders++;
54     }
55     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
56   } else {
57     my %error_msg  = (
58       shop_id          => $self->config->id,
59       shop_description => $self->config->description,
60       message          => $answer->{message},
61       error            => 1,
62     );
63     %fetched_orders = %error_msg;
64   }
65   return \%fetched_orders;
66 }
67
68 sub get_new_orders {
69   my ($self) = @_;
70
71   my $dbh       = SL::DB::client;
72   my $otf              = $self->config->orders_to_fetch || 10;
73   my $number_of_orders = 0;
74   my @errors;
75
76   my $answer = $self->send_request(
77     "orders",
78     undef,
79     "get",
80     "&per_page=$otf&status=pending"
81   );
82   my %fetched_orders;
83   if($answer->{success}) {
84     my $orders = $answer->{data};
85     foreach my $shoporder(@{$orders}){
86
87       $dbh->with_transaction( sub{
88           #update status on server
89           $shoporder->{status} = "processing";
90           my $anser = $self->set_orderstatus($$shoporder->{id}, "fetched");
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 $answer= $self->send_request("taxes");
135   unless ($answer->{success}){ return 0;}
136   my %taxes = map { ($_->{id} => $_) } @{ $answer->{data} };
137
138   my $active_price_source = $self->config->price_source;
139   #Mapping Positions
140   foreach my $pos(@positions) {
141     my $price = $::form->round_amount($pos->{total},2);
142     my $tax_id = $pos->{taxes}[0]->{id};
143     my $tax_rate = $taxes{ $tax_id }{rate};
144     my %pos_columns = ( description          => $pos->{name},
145                         partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
146                         price                => $price,
147                         quantity             => $pos->{quantity},
148                         position             => $position,
149                         tax_rate             => $tax_rate,
150                         shop_trans_id        => $pos->{product_id},
151                         shop_order_id        => $id,
152                         active_price_source  => $active_price_source,
153                       );
154     #$main::lxdebug->dump(0, "TST: WooCommerce save pos", $pos);
155     #$main::lxdebug->dump(0, "TST: WooCommerce save pos_columns", \%pos_columns);
156     my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
157     $pos_insert->save;
158     $position++;
159   }
160   $shop_order->positions($position-1);
161
162   my $customer = $shop_order->get_customer;
163
164   if(ref($customer)){
165     $shop_order->kivi_customer_id($customer->id);
166   }
167   $shop_order->save;
168 }
169
170
171 sub map_data_to_shoporder {
172   my ($self, $import) = @_;
173
174   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
175                                                   locale    => 'de_DE',
176                                                   time_zone => 'local'
177                                                 );
178
179   my $shop_id      = $self->config->id;
180
181   # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
182   my %columns = (
183 #billing
184     billing_firstname       => $import->{billing}->{first_name},
185     billing_lastname        => $import->{billing}->{last_name},
186     #address_1 address_2
187     billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
188     # ???
189     billing_city            => $import->{billing}->{city},
190     #state
191     # ???
192     billing_zipcode         => $import->{billing}->{postcode},
193     billing_country         => $import->{billing}->{country},
194     billing_email           => $import->{billing}->{email},
195     billing_phone           => $import->{billing}->{phone},
196
197     #billing_greeting        => "",
198     #billing_fax             => "",
199     #billing_vat             => "",
200     #billing_company         => "",
201     #billing_department      => "",
202
203 #customer
204     #customer_id
205     shop_customer_id        => $import->{customer_id},
206     shop_customer_number    => $import->{customer_id},
207     #customer_ip_address
208     remote_ip               => $import->{customer_ip_address},
209     #customer_user_agent
210     #customer_note
211     shop_customer_comment   => $import->{customer_note},
212
213     #customer_city           => "",
214     #customer_company        => "",
215     #customer_country        => "",
216     #customer_department     => "",
217     #customer_email          => "",
218     #customer_fax            => "",
219     #customer_firstname      => "",
220     #customer_greeting       => "",
221     #customer_lastname       => "",
222     #customer_phone          => "",
223     #customer_street         => "",
224     #customer_vat            => "",
225
226 #shipping
227     delivery_firstname      => $import->{shipping}->{first_name},
228     delivery_lastname       => $import->{shipping}->{last_name},
229     delivery_company        => $import->{shipping}->{company},
230     #address_1 address_2
231     delivery_street         => $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : ""),
232     delivery_city           => $import->{shipping}->{city},
233     #state ???
234     delivery_zipcode        => $import->{shipping}->{postcode},
235     delivery_country        => $import->{shipping}->{country},
236     #delivery_department     => "",
237     #delivery_email          => "",
238     #delivery_fax            => "",
239     #delivery_phone          => "",
240     #delivery_vat            => "",
241
242 #other
243     #id
244     #parent_id
245     #number
246     shop_ordernumber        => $import->{number},
247     #order_key
248     #created_via
249     #version
250     #status
251     #currency
252     #date_created
253     order_date              => $parser->parse_datetime($import->{date_created}),
254     #date_created_gmt
255     #date_modified
256     #date_modified_gmt
257     #discount_total
258     #discount_tax
259     #shipping_total
260     shipping_costs          => $import->{shipping_costs},
261     #shipping_tax
262     shipping_costs_net      => $import->{shipping_costs} - $import->{shipping_tax},
263     #cart_tax
264     #total
265     amount                  => $import->{total},
266     #total_tax
267     netamount               => $import->{total} - $import->{total_tax},
268     #prices_include_tax
269     tax_included            => $import->{prices_include_tax} eq "true" ? 1 : 0,
270     #payment_method
271     # ??? payment_id              => $import->{payment_method},
272     #payment_method_title
273     payment_description     => $import->{payment}->{payment_method_title},
274     #transaction_id
275     shop_trans_id           => $import->{id},
276     #date_paid
277     #date_paid_gmt
278     #date_completed
279     #date_completed_gmt
280
281     host                    => $import->{_links}->{self}[0]->{href},
282
283     #sepa_account_holder     => "",
284     #sepa_bic                => "",
285     #sepa_iban               => "",
286
287     #shop_c_billing_id       => "",
288     #shop_c_billing_number   => "",
289     shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
290
291 # not in Shop
292     shop_id                 => $shop_id,
293   );
294
295   my $shop_order = SL::DB::ShopOrder->new(%columns);
296   return $shop_order;
297 }
298
299 #TODO CVARS, tax and images
300 sub update_part {
301   my ($self, $shop_part, $todo) = @_;
302
303   #shop_part is passed as a param
304   die unless ref($shop_part) eq 'SL::DB::ShopPart';
305   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
306
307   # CVARS to map
308   #my $cvars = {
309   #  map {
310   #    ($_->config->name => {
311   #      value => $_->value_as_text,
312   #      is_valid => $_->is_valid
313   #    })
314   #  }
315   #  @{ $part->cvars_by_config }
316   #};
317
318   my @categories = ();
319   if ($shop_part->shop_category) {
320     foreach my $row_cat ( @{ $shop_part->shop_category } ) {
321       my $temp = { ( id => @{$row_cat}[0], ) };
322       push ( @categories, $temp );
323     }
324   }
325
326   #my @upload_img = $shop_part->get_images;
327   my $partnumber = $::form->escape($part->partnumber);#don't accept / in partnumber
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 partnumber
387   #  my $imgup      = $self->connector->put($url . "api/generatepartimages/$partnumber?useNumberAsId=true");
388   #}
389
390   return $answer->{success};
391 }
392
393 sub get_article_info {
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   $answer->{data} = $answer->{data}[0];
401   #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info ", $answer);
402   return $answer;
403
404   if($answer->{success} && scalar @{$answer->{data}}){
405     my $part = $self->map_data_to_part($answer->{data}[0]);
406     #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info part", $part);
407     return $part;
408   } else {
409     #What shut be here?
410     return $answer
411   }
412 }
413
414 sub map_data_to_part {
415   my ($self, $part_data) = @_;
416
417   my %map_part_data =  (
418     #id                 => $part_data->{},
419     partnumber         => $part_data->{sku},
420     description        => $part_data->{name},
421     #listprice          => $part_data->{},
422     #sellprice          => $part_data->{},
423     #lastcost           => $part_data->{},
424     #priceupdate        => $part_data->{},
425     #weight             => $part_data->{},
426     notes              => $part_data->{description},
427     #makemodel          => $part_data->{},
428     #rop                => $part_data->{},
429     shop               => 1,
430     obsolete           => 0,
431     #bom                => $part_data->{},
432     #image              => $part_data->{},
433     #drawing            => $part_data->{},
434     #microfiche         => $part_data->{},
435     #partsgroup_id      => $part_data->{},
436     #ve                 => $part_data->{},
437     #gv                 => $part_data->{},
438     #itime              => $part_data->{},
439     #mtime              => $part_data->{},
440     #unit               => $part_data->{},
441     unit               => 'Stck',
442     #formel             => $part_data->{},
443     #not_discountable   => $part_data->{},
444     #buchungsgruppen_id => $part_data->{},
445     #payment_id         => $part_data->{},
446     #ean                => $part_data->{},
447     #price_factor_id    => $part_data->{},
448     #onhand             => $part_data->{},
449     #stockable          => $part_data->{},
450     #has_sernumber      => $part_data->{},
451     #warehouse_id       => $part_data->{},
452     #bin_id             => $part_data->{},
453     #df_status_aktuell  => $part_data->{},
454     #df_status_verlauf  => $part_data->{},
455     #active             => $part_data->{},
456     #classification_id  => $part_data->{},
457     part_type          => 'part',
458   );
459   return SL::DB::Part->new(%map_part_data);
460 }
461
462 sub map_data_to_shop_part {
463   my ($self, $part_data, $part) = @_;
464
465   my @categories = ();
466   foreach my $row_cat ( @{ $part_data->{categories} } ) {
467     my @tmp;
468     push( @tmp,$row_cat->{id} );
469     push( @tmp,$row_cat->{name} );
470     push( @categories,\@tmp );
471   }
472   my %map_shop_part_data =  (
473     #id                  => ,
474     shop_id             => $self->config->id,
475     part_id             => $part->id,
476     shop_description    => $part_data->{description},
477     #itime               => ,
478     #mtime               => ,
479     #last_update         => ,
480     #show_date           => ,
481     sortorder           => $part_data->{menu_order},
482     #front_page          => ,
483     active              => 1,
484     shop_category       => \@categories,
485     #active_price_source => ,
486     #metatag_keywords    => ,
487     #metatag_description => ,
488     #metatag_title       => ,
489     #shop_versandhinweis => ,
490   );
491   return SL::DB::ShopPart->new(%map_shop_part_data);
492 }
493 sub get_shop_parts {
494   my ($self, $partnumber) = @_;
495
496   my $dbh       = SL::DB::client;
497   my @errors;
498   my $number_of_parts = 0;
499   my %fetched_parts;
500   my $answer;
501
502   if ($partnumber) {
503     $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
504     $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
505   } else {
506     #TODO
507     $answer = $self->send_request("products/", undef , "get");
508     if ($answer->{total_pages} > 1) {
509       my $current_page = 2;
510       while ($current_page <= $answer->{total_pages}) {
511         my $tmp_answer = $self->send_request("products/", undef , "get", "&page=$current_page");
512         foreach my $part (@{$tmp_answer->{data}}) {
513           push @{$answer->{data}} , $part;
514         }
515         $current_page++;
516       }
517     }
518   }
519
520   if($answer->{success} && scalar @{$answer->{data}}){
521     $dbh->with_transaction( sub{
522       foreach my $part_data (@{$answer->{data}}) {
523       unless (!$part_data->{sku} || SL::DB::Manager::Part->get_all_count( query => [ partnumber => $part_data->{sku} ] )) {
524           my $part = $self->map_data_to_part($part_data);
525           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts part ", $part);
526           $part->save;
527           my $shop_part = $self->map_data_to_shop_part($part_data, $part);
528           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts shop_part ", $shop_part);
529           $shop_part->save;
530           $number_of_parts++;
531         }
532       }
533       return 1;
534     })or do {
535       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
536     };
537
538     if(@errors){
539       flash_later('error', $::locale->text('Errors: #1', @errors));
540     }
541     %fetched_parts = (
542       shop_id          => $self->config->id,
543       shop_description => $self->config->description,
544       number_of_parts => $number_of_parts,
545     );
546   } else {
547     my %error_msg  = (
548       shop_id          => $self->config->id,
549       shop_description => $self->config->description,
550       message          => $answer->{message},
551       error            => 1,
552     );
553     %fetched_parts = %error_msg;
554   }
555   return \%fetched_parts;
556 }
557
558 sub get_categories {
559   my ($self) = @_;
560
561   my $answer = $self->send_request("products/categories");
562   unless($answer->{success}) {
563     return $answer;
564   }
565   my @data = @{$answer->{data}};
566   my %categories = map { ($_->{id} => $_) } @data;
567
568   my @categories_tree;
569   for(@data) {
570     my $parent = $categories{$_->{parent}};
571     if($parent) {
572       $parent->{children} ||= [];
573       push @{$parent->{children}},$_;
574     } else {
575       push @categories_tree, $_;
576     }
577   }
578
579   return \@categories_tree;
580 }
581
582 sub get_version {
583   my ($self) = @_;
584
585   my $answer = $self->send_request("system_status");
586   if($answer->{success}) {
587     my $version = $answer->{data}->{environment}->{version};
588     my %return = (
589       success => 1,
590       data    => { version => $version },
591     );
592     return \%return;
593   } else {
594     return $answer;
595   }
596 }
597
598 sub set_orderstatus {
599   my ($self,$order_id, $status) = @_;
600   if ($status eq "fetched") { $status =  "processing"; }
601   if ($status eq "completed") { $status = "completed"; }
602   my %new_status = (status => $status);
603   my $status_json = SL::JSON::to_json( \%new_status);
604   my $answer = $self->send_request("orders/$order_id", $status_json, "put");
605   unless($answer->{success}){
606     return 0;
607   }
608   return 1;
609 }
610
611 sub create_url {
612   my ($self) = @_;
613   my $request = $_[1];
614   my $parameters = $_[2];
615
616   my $consumer_key = $self->config->login;
617   my $consumer_secret = $self->config->password;
618   my $protocol = $self->config->protocol;
619   my $server = $self->config->server;
620   my $port = $self->config->port;
621   my $path = $self->config->path;
622
623   return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
624 }
625
626 sub send_request {
627   my ($self) = @_;
628   my $request = $_[1];
629   my $json_data = $_[2];
630   my $method_type = $_[3];
631   my $parameters = $_[4];
632
633   my $ua = LWP::UserAgent->new;
634   my $url = $self->create_url( $request, $parameters );
635
636   my $answer;
637   if( $method_type eq "put" ) {
638     $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
639   } elsif ( $method_type eq "post") {
640     $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
641   } else {
642     $answer = $ua->get($url);
643   }
644
645   my $type = $answer->content_type;
646   my $status_line = $answer->status_line;
647
648   my %return;
649   if($answer->is_success && $type eq 'application/json'){
650     my $data_json = $answer->content;
651     #$main::lxdebug->dump(0, "TST: WooCommerce send_request header ", $answer->header( 'Link'));
652     my $json = SL::JSON::decode_json($data_json);
653     %return = (
654       success => 1,
655       data    => $json,
656       total_pages => $answer->header( 'X-WP-TotalPages'),
657       total_elements => $answer->header( 'X-WP-Total'),
658     );
659   }else{
660     %return = (
661       success => 0,
662       data    => { version => $url . ": " . $status_line, data_type => $type },
663       message => "Error: $status_line",
664     );
665   }
666   #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);
667   return \%return;
668
669 }
670
671 1;