Webshop Order Zahlungsbedingen mit übergeben
[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}, "completed");
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=processing&after=2020-12-31T23:59:59&order=asc"
81   );
82   my %fetched_orders;
83   if($answer->{success}) {
84     my $orders = $answer->{data};
85     foreach my $shoporder(@{$orders}){
86       $dbh->with_transaction( sub{
87           #update status on server
88           $shoporder->{status} = "completed";
89           my $anwser = $self->set_orderstatus($shoporder->{id}, "completed");
90           unless($answer){
91             push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
92             return 0;
93           }
94
95           unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
96
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         flash_later('error', $::locale->text('Errors: #1', @errors));
104       } else {
105         $number_of_orders++;
106       }
107     }
108     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
109
110   } else {
111     my %error_msg  = (
112       shop_id          => $self->config->id,
113       shop_description => $self->config->description,
114       message          => $answer->{message},
115       error            => 1,
116     );
117     %fetched_orders = %error_msg;
118   }
119
120   return \%fetched_orders;
121 }
122
123 sub import_data_to_shop_order {
124   my ( $self, $import ) = @_;
125   my $shop_order = $self->map_data_to_shoporder($import);
126
127   $shop_order->save;
128   my $id = $shop_order->id;
129
130   my @positions = sort { Sort::Naturally::ncmp($a->{"sku"}, $b->{"sku"}) } @{ $import->{line_items} };
131   my $position = 1;
132
133   my $active_price_source = $self->config->price_source;
134   my $tax_included = $self->config->pricetype eq 'brutto' ? 1 : 0;
135   #Mapping Positions
136   foreach my $pos(@positions) {
137     my $tax_rate = $pos->{tax_class} eq "reduced-rate" ? 7 : 19;
138     my $tax_factor = $tax_rate/100+1;
139     my $price = $pos->{price};
140     if ( $tax_included ) {
141       $price = $price * $tax_factor;
142       $price = $::form->round_amount($price,2);
143     } else {
144       $price = $::form->round_amount($price,2);
145     }
146     my %pos_columns = ( description          => $pos->{name},
147                         partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
148                         price                => $price,
149                         quantity             => $pos->{quantity},
150                         position             => $position,
151                         tax_rate             => $tax_rate,
152                         shop_trans_id        => $pos->{product_id},
153                         shop_order_id        => $id,
154                         active_price_source  => $active_price_source,
155                       );
156     #$main::lxdebug->dump(0, "TST: WooCommerce save pos", $pos);
157     #$main::lxdebug->dump(0, "TST: WooCommerce save pos_columns", \%pos_columns);
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::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                               'paypal'                            => 2489,
209                               'german_market_purchase_on_account' => 2487,
210                             );
211   my %columns = (
212 #billing Shop can have different billing addresses, and may have 1 customer_address
213     billing_firstname       => $import->{billing}->{first_name},
214     billing_lastname        => $import->{billing}->{last_name},
215     #address_1 address_2
216     billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
217     # ???
218     billing_city            => $import->{billing}->{city},
219     #state
220     # ???
221     billing_zipcode         => $import->{billing}->{postcode},
222     billing_country         => $import->{billing}->{country},
223     billing_email           => $import->{billing}->{email},
224     billing_phone           => $import->{billing}->{phone},
225
226     #billing_greeting        => "",
227     #billing_fax             => "",
228     #billing_vat             => "",
229     billing_company         => $import->{billing}->{company},
230     #billing_department      => "",
231
232 #customer
233     #customer_id
234     shop_customer_id        => $import->{customer_id},
235     shop_customer_number    => $import->{customer_id},
236     #customer_ip_address
237     remote_ip               => $import->{customer_ip_address},
238     #customer_user_agent
239     #customer_note
240     shop_customer_comment   => $import->{customer_note},
241
242     #customer_city           => "",
243     #customer_company        => "",
244     #customer_country        => "",
245     #customer_department     => "",
246     #customer_email          => "",
247     #customer_fax            => "",
248     #customer_firstname      => "",
249     #customer_greeting       => "",
250     #customer_lastname       => "",
251     #customer_phone          => "",
252     #customer_street         => "",
253     #customer_vat            => "",
254
255 #shipping
256     delivery_firstname      => $import->{shipping}->{first_name} || $import->{billing}->{first_name},
257     delivery_lastname       => $import->{shipping}->{last_name} || $import->{billing}->{last_name},
258     delivery_company        => $import->{shipping}->{company} || $import->{billing}->{company},
259     #address_1 address_2
260     delivery_street         => $d_street,
261     delivery_city           => $import->{shipping}->{city} || $import->{billing}->{city},
262     #state ???
263     delivery_zipcode        => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
264     delivery_country        => $import->{shipping}->{country} || $import->{billing}->{country},
265     #delivery_department     => "",
266     #delivery_email          => "",
267     #delivery_fax            => "",
268     #delivery_phone          => "",
269     #delivery_vat            => "",
270
271 #other
272     #id
273     #parent_id
274     #number
275     shop_ordernumber        => $import->{number},
276     #order_key
277     #created_via
278     #version
279     #status
280     #currency
281     #date_created
282     order_date              => $parser->parse_datetime($import->{date_created}),
283     #date_created_gmt
284     #date_modified
285     #date_modified_gmt
286     #discount_total
287     #discount_tax
288     #shipping_total
289     shipping_costs          => $import->{shipping_total},
290     #shipping_tax
291     shipping_costs_net      => $import->{shipping_total},
292     #cart_tax
293     #total
294     amount                  => $import->{total},
295     #total_tax
296     netamount               => $import->{total} - $import->{total_tax},
297     #prices_include_tax
298     tax_included            => $tax_included,
299     #payment_method
300     payment_id              => $payment_ids_methods{$import->{payment_method}} || 2487,
301     #payment_method_title
302     payment_description     => $import->{payment_method_title},
303     #transaction_id
304     shop_trans_id           => $import->{id},
305     #date_paid
306     #date_paid_gmt
307     #date_completed
308     #date_completed_gmt
309
310     host                    => $import->{_links}->{self}[0]->{href},
311
312     #sepa_account_holder     => "",
313     #sepa_bic                => "",
314     #sepa_iban               => "",
315
316     #shop_c_billing_id       => "",
317     #shop_c_billing_number   => "",
318     shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
319
320 # not in Shop
321     shop_id                 => $shop_id,
322   );
323
324   my $shop_order = SL::DB::ShopOrder->new(%columns);
325   return $shop_order;
326 }
327
328 #TODO CVARS, tax and images
329 sub update_part {
330   my ($self, $shop_part, $todo) = @_;
331
332   #shop_part is passed as a param
333   die unless ref($shop_part) eq 'SL::DB::ShopPart';
334   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
335
336   # CVARS to map
337   #my $cvars = {
338   #  map {
339   #    ($_->config->name => {
340   #      value => $_->value_as_text,
341   #      is_valid => $_->is_valid
342   #    })
343   #  }
344   #  @{ $part->cvars_by_config }
345   #};
346
347   my @categories = ();
348   if ($shop_part->shop_category) {
349     foreach my $row_cat ( @{ $shop_part->shop_category } ) {
350       my $temp = { ( id => @{$row_cat}[0], ) };
351       push ( @categories, $temp );
352     }
353   }
354
355   #my @upload_img = $shop_part->get_images;
356   my $partnumber = $::form->escape($part->partnumber);#don't accept / in partnumber
357   my $stock_status = ($part->onhand ? "instock" : "outofstock");
358   my $status = ($shop_part->active ? "publish" : "private");
359   my $tax_n_price = $shop_part->get_tax_and_price;
360   my $price = $tax_n_price->{price};
361   #my $taxrate = $tax_n_price->{tax};
362   #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
363
364   my %shop_data;
365
366   if($todo eq "price"){
367     %shop_data = (
368       regular_price => $price,
369     );
370   }elsif($todo eq "stock"){
371     %shop_data = (
372       stock_status => $stock_status,
373     );
374   }elsif($todo eq "price_stock"){
375     %shop_data =  (
376       stock_status => $stock_status,
377       regular_price => $price,
378     );
379   }elsif($todo eq "active"){
380     %shop_data =  (
381       status => $status,
382     );
383   }elsif($todo eq "all"){
384   # mapping  still missing attributes,metatags
385     %shop_data =  (
386       sku => $partnumber,
387       name => $part->description,
388       stock_status => $stock_status,
389       regular_price => $price,
390       status => $status,
391       description=> $shop_part->shop_description,
392       short_description=> $shop_part->shop_description,
393       categories => [ @categories ],
394       #tax_class => $tax_class,
395     );
396   }
397
398   my $dataString = SL::JSON::to_json(\%shop_data);
399   $dataString    = encode_utf8($dataString);
400
401   # LWP->post = create || LWP->put = update
402   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
403
404   if($answer->{success} && scalar @{$answer->{data}}){
405     #update
406     my $woo_shop_part_id = $answer->{data}[0]->{id};
407     $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
408   }else{
409     #upload
410     $answer = $self->send_request("products", $dataString, "post");
411   }
412
413   # don't know if this is needed
414   #if(@upload_img) {
415   #  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in partnumber
416   #  my $imgup      = $self->connector->put($url . "api/generatepartimages/$partnumber?useNumberAsId=true");
417   #}
418
419   return $answer->{success};
420 }
421
422 sub get_article_info {
423   my ($self) = @_;
424   my $partnumber = $_[1];
425
426   $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
427   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
428
429   $answer->{data} = $answer->{data}[0];
430   #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info ", $answer);
431   return $answer;
432
433   if($answer->{success} && scalar @{$answer->{data}}){
434     my $part = $self->map_data_to_part($answer->{data}[0]);
435     #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info part", $part);
436     return $part;
437   } else {
438     #What shut be here?
439     return $answer
440   }
441 }
442
443 sub map_data_to_part {
444   my ($self, $part_data) = @_;
445
446   my %map_part_data =  (
447     #id                 => $part_data->{},
448     partnumber         => $part_data->{sku},
449     description        => $part_data->{name},
450     #listprice          => $part_data->{},
451     #sellprice          => $part_data->{},
452     #lastcost           => $part_data->{},
453     #priceupdate        => $part_data->{},
454     #weight             => $part_data->{},
455     notes              => $part_data->{description},
456     #makemodel          => $part_data->{},
457     #rop                => $part_data->{},
458     shop               => 1,
459     obsolete           => 0,
460     #bom                => $part_data->{},
461     #image              => $part_data->{},
462     #drawing            => $part_data->{},
463     #microfiche         => $part_data->{},
464     #partsgroup_id      => $part_data->{},
465     #ve                 => $part_data->{},
466     #gv                 => $part_data->{},
467     #itime              => $part_data->{},
468     #mtime              => $part_data->{},
469     #unit               => $part_data->{},
470     unit               => 'Stck',
471     #formel             => $part_data->{},
472     #not_discountable   => $part_data->{},
473     #buchungsgruppen_id => $part_data->{},
474     #payment_id         => $part_data->{},
475     #ean                => $part_data->{},
476     #price_factor_id    => $part_data->{},
477     #onhand             => $part_data->{},
478     #stockable          => $part_data->{},
479     #has_sernumber      => $part_data->{},
480     #warehouse_id       => $part_data->{},
481     #bin_id             => $part_data->{},
482     #df_status_aktuell  => $part_data->{},
483     #df_status_verlauf  => $part_data->{},
484     #active             => $part_data->{},
485     #classification_id  => $part_data->{},
486     part_type          => 'part',
487   );
488   return SL::DB::Part->new(%map_part_data);
489 }
490
491 sub map_data_to_shop_part {
492   my ($self, $part_data, $part) = @_;
493
494   my @categories = ();
495   foreach my $row_cat ( @{ $part_data->{categories} } ) {
496     my @tmp;
497     push( @tmp,$row_cat->{id} );
498     push( @tmp,$row_cat->{name} );
499     push( @categories,\@tmp );
500   }
501   my %map_shop_part_data =  (
502     #id                  => ,
503     shop_id             => $self->config->id,
504     part_id             => $part->id,
505     shop_description    => $part_data->{description},
506     #itime               => ,
507     #mtime               => ,
508     #last_update         => ,
509     #show_date           => ,
510     sortorder           => $part_data->{menu_order},
511     #front_page          => ,
512     active              => 1,
513     shop_category       => \@categories,
514     #active_price_source => ,
515     #metatag_keywords    => ,
516     #metatag_description => ,
517     #metatag_title       => ,
518     #shop_versandhinweis => ,
519   );
520   return SL::DB::ShopPart->new(%map_shop_part_data);
521 }
522 sub get_shop_parts {
523   my ($self, $partnumber) = @_;
524
525   my $dbh       = SL::DB::client;
526   my @errors;
527   my $number_of_parts = 0;
528   my %fetched_parts;
529   my $answer;
530
531   if ($partnumber) {
532     $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
533     $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
534   } else {
535     #TODO
536     $answer = $self->send_request("products/", undef , "get");
537     if ($answer->{total_pages} > 1) {
538       my $current_page = 2;
539       while ($current_page <= $answer->{total_pages}) {
540         my $tmp_answer = $self->send_request("products/", undef , "get", "&page=$current_page");
541         foreach my $part (@{$tmp_answer->{data}}) {
542           push @{$answer->{data}} , $part;
543         }
544         $current_page++;
545       }
546     }
547   }
548
549   if($answer->{success} && scalar @{$answer->{data}}){
550     $dbh->with_transaction( sub{
551       foreach my $part_data (@{$answer->{data}}) {
552       unless (!$part_data->{sku} || SL::DB::Manager::Part->get_all_count( query => [ partnumber => $part_data->{sku} ] )) {
553           my $part = $self->map_data_to_part($part_data);
554           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts part ", $part);
555           $part->save;
556           my $shop_part = $self->map_data_to_shop_part($part_data, $part);
557           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts shop_part ", $shop_part);
558           $shop_part->save;
559           $number_of_parts++;
560         }
561       }
562       return 1;
563     })or do {
564       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
565     };
566
567     if(@errors){
568       flash_later('error', $::locale->text('Errors: #1', @errors));
569     }
570     %fetched_parts = (
571       shop_id          => $self->config->id,
572       shop_description => $self->config->description,
573       number_of_parts => $number_of_parts,
574     );
575   } else {
576     my %error_msg  = (
577       shop_id          => $self->config->id,
578       shop_description => $self->config->description,
579       message          => $answer->{message},
580       error            => 1,
581     );
582     %fetched_parts = %error_msg;
583   }
584   return \%fetched_parts;
585 }
586
587 sub get_categories {
588   my ($self) = @_;
589
590   my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
591   unless($answer->{success}) {
592     return $answer;
593   }
594   my @data = @{$answer->{data}};
595   my %categories = map { ($_->{id} => $_) } @data;
596
597   my @categories_tree;
598   for(@data) {
599     my $parent = $categories{$_->{parent}};
600     if($parent) {
601       $parent->{children} ||= [];
602       push @{$parent->{children}},$_;
603     } else {
604       push @categories_tree, $_;
605     }
606   }
607
608   return \@categories_tree;
609 }
610
611 sub get_version {
612   my ($self) = @_;
613
614   my $answer = $self->send_request("system_status");
615   if($answer->{success}) {
616     my $version = $answer->{data}->{environment}->{version};
617     my %return = (
618       success => 1,
619       data    => { version => $version },
620     );
621     return \%return;
622   } else {
623     return $answer;
624   }
625 }
626
627 sub set_orderstatus {
628   my ($self,$order_id, $status) = @_;
629   #  if ($status eq "fetched") { $status =  "processing"; }
630   #  if ($status eq "processing") { $status = "completed"; }
631   my %new_status = (status => $status);
632   my $status_json = SL::JSON::to_json( \%new_status);
633   my $answer = $self->send_request("orders/$order_id", $status_json, "put");
634   unless($answer->{success}){
635     return 0;
636   }
637   return 1;
638 }
639
640 sub create_url {
641   my ($self) = @_;
642   my $request = $_[1];
643   my $parameters = $_[2];
644
645   my $consumer_key = $self->config->login;
646   my $consumer_secret = $self->config->password;
647   my $protocol = $self->config->protocol;
648   my $server = $self->config->server;
649   my $port = $self->config->port;
650   my $path = $self->config->path;
651
652   return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
653 }
654
655 sub send_request {
656   my ($self) = @_;
657   my $request = $_[1];
658   my $json_data = $_[2];
659   my $method_type = $_[3];
660   my $parameters = $_[4];
661
662   my $ua = LWP::UserAgent->new;
663   my $url = $self->create_url( $request, $parameters );
664
665   my $answer;
666   if( $method_type eq "put" ) {
667     $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
668   } elsif ( $method_type eq "post") {
669     $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
670   } else {
671     $answer = $ua->get($url);
672   }
673
674   my $type = $answer->content_type;
675   my $status_line = $answer->status_line;
676
677   my %return;
678   if($answer->is_success && $type eq 'application/json'){
679     my $data_json = $answer->content;
680     #$main::lxdebug->dump(0, "TST: WooCommerce send_request header ", $answer->header( 'Link'));
681     my $json = SL::JSON::decode_json($data_json);
682     %return = (
683       success => 1,
684       data    => $json,
685       total_pages => $answer->header( 'X-WP-TotalPages'),
686       total_elements => $answer->header( 'X-WP-Total'),
687     );
688   }else{
689     %return = (
690       success => 0,
691       data    => { version => $url . ": " . $status_line, data_type => $type },
692       message => "Error: $status_line",
693     );
694   }
695   #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);
696   return \%return;
697
698 }
699
700 1;