1 package SL::ShopConnector::WooCommerce;
 
   5 use parent qw(SL::ShopConnector::Base);
 
   9 use LWP::Authen::Digest;
 
  10 use SL::DB::ShopOrder;
 
  11 use SL::DB::ShopOrderItem;
 
  15 use SL::Helper::Flash;
 
  16 use Encode qw(encode_utf8);
 
  19   my ($self, $order_id) = @_;
 
  21   my $dbh       = SL::DB::client;
 
  22   my $number_of_orders = 0;
 
  25   my $answer = $self->send_request(
 
  26     "orders/" . $order_id,
 
  31   if($answer->{success}) {
 
  32     my $shoporder = $answer->{data};
 
  34     $dbh->with_transaction( sub{
 
  35         #update status on server
 
  36         $shoporder->{status} = "processing";
 
  37         my $answer = $self->set_orderstatus($shoporder->{id}, "completed");
 
  39           push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
 
  43         unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
 
  47       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
 
  51       flash_later('error', $::locale->text('Errors: #1', @errors));
 
  55     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
 
  58       shop_id          => $self->config->id,
 
  59       shop_description => $self->config->description,
 
  60       message          => $answer->{message},
 
  63     %fetched_orders = %error_msg;
 
  65   return \%fetched_orders;
 
  71   my $dbh       = SL::DB::client;
 
  72   my $otf              = $self->config->orders_to_fetch || 10;
 
  73   my $number_of_orders = 0;
 
  76   my $answer = $self->send_request(
 
  80     "&per_page=$otf&status=processing&after=2020-12-31T23:59:59&order=asc"
 
  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");
 
  91             push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
 
  95           unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
 
  99         push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
 
 103         flash_later('error', $::locale->text('Errors: #1', @errors));
 
 108     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
 
 112       shop_id          => $self->config->id,
 
 113       shop_description => $self->config->description,
 
 114       message          => $answer->{message},
 
 117     %fetched_orders = %error_msg;
 
 120   return \%fetched_orders;
 
 123 sub import_data_to_shop_order {
 
 124   my ( $self, $import ) = @_;
 
 125   my $shop_order = $self->map_data_to_shoporder($import);
 
 128   my $id = $shop_order->id;
 
 130   my @positions = sort { Sort::Naturally::ncmp($a->{"sku"}, $b->{"sku"}) } @{ $import->{line_items} };
 
 133   my $active_price_source = $self->config->price_source;
 
 134   my $tax_included = $self->config->pricetype eq 'brutto' ? 1 : 0;
 
 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);
 
 144       $price = $::form->round_amount($price,2);
 
 146     my %pos_columns = ( description          => $pos->{name},
 
 147                         partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
 
 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,
 
 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);
 
 162   $shop_order->positions($position-1);
 
 164   if ( $self->config->shipping_costs_parts_id ) {
 
 165     my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
 
 167       description    => $import->{data}->{dispatch}->{name},
 
 168       partnumber     => $shipping_part->partnumber,
 
 169       price          => $import->{data}->{invoiceShipping},
 
 171       position       => $position,
 
 173       shop_order_id  => $id,
 
 175     my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
 
 176     $shipping_pos_insert->save;
 
 179   my $customer = $shop_order->get_customer;
 
 182     $shop_order->kivi_customer_id($customer->id);
 
 188 sub map_data_to_shoporder {
 
 189   my ($self, $import) = @_;
 
 191   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
 
 196   my $shop_id      = $self->config->id;
 
 197   my $tax_included = $self->config->pricetype;
 
 199   # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
 
 201     if ( $import->{shipping}->{address_1} ne "" ) {
 
 202       $d_street = $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : "");
 
 204       $d_street = $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : "");
 
 207 #billing Shop can have different billing addresses, and may have 1 customer_address
 
 208     billing_firstname       => $import->{billing}->{first_name},
 
 209     billing_lastname        => $import->{billing}->{last_name},
 
 211     billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
 
 213     billing_city            => $import->{billing}->{city},
 
 216     billing_zipcode         => $import->{billing}->{postcode},
 
 217     billing_country         => $import->{billing}->{country},
 
 218     billing_email           => $import->{billing}->{email},
 
 219     billing_phone           => $import->{billing}->{phone},
 
 221     #billing_greeting        => "",
 
 224     billing_company         => $import->{billing}->{company},
 
 225     #billing_department      => "",
 
 229     shop_customer_id        => $import->{customer_id},
 
 230     shop_customer_number    => $import->{customer_id},
 
 232     remote_ip               => $import->{customer_ip_address},
 
 235     shop_customer_comment   => $import->{customer_note},
 
 237     #customer_city           => "",
 
 238     #customer_company        => "",
 
 239     #customer_country        => "",
 
 240     #customer_department     => "",
 
 241     #customer_email          => "",
 
 243     #customer_firstname      => "",
 
 244     #customer_greeting       => "",
 
 245     #customer_lastname       => "",
 
 246     #customer_phone          => "",
 
 247     #customer_street         => "",
 
 251     delivery_firstname      => $import->{shipping}->{first_name} || $import->{billing}->{first_name},
 
 252     delivery_lastname       => $import->{shipping}->{last_name} || $import->{billing}->{last_name},
 
 253     delivery_company        => $import->{shipping}->{company} || $import->{billing}->{company},
 
 255     delivery_street         => $d_street,
 
 256     delivery_city           => $import->{shipping}->{city} || $import->{billing}->{city},
 
 258     delivery_zipcode        => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
 
 259     delivery_country        => $import->{shipping}->{country} || $import->{billing}->{country},
 
 260     #delivery_department     => "",
 
 261     #delivery_email          => "",
 
 263     #delivery_phone          => "",
 
 270     shop_ordernumber        => $import->{number},
 
 277     order_date              => $parser->parse_datetime($import->{date_created}),
 
 284     shipping_costs          => $import->{shipping_total},
 
 286     shipping_costs_net      => $import->{shipping_total},
 
 289     amount                  => $import->{total},
 
 291     netamount               => $import->{total} - $import->{total_tax},
 
 293     tax_included            => $tax_included,
 
 295     # ??? payment_id              => $import->{payment_method},
 
 296     #payment_method_title
 
 297     payment_description     => $import->{payment}->{payment_method_title},
 
 299     shop_trans_id           => $import->{id},
 
 305     host                    => $import->{_links}->{self}[0]->{href},
 
 307     #sepa_account_holder     => "",
 
 311     #shop_c_billing_id       => "",
 
 312     #shop_c_billing_number   => "",
 
 313     shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
 
 319   my $shop_order = SL::DB::ShopOrder->new(%columns);
 
 323 #TODO CVARS, tax and images
 
 325   my ($self, $shop_part, $todo) = @_;
 
 327   #shop_part is passed as a param
 
 328   die unless ref($shop_part) eq 'SL::DB::ShopPart';
 
 329   my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
 
 334   #    ($_->config->name => {
 
 335   #      value => $_->value_as_text,
 
 336   #      is_valid => $_->is_valid
 
 339   #  @{ $part->cvars_by_config }
 
 343   if ($shop_part->shop_category) {
 
 344     foreach my $row_cat ( @{ $shop_part->shop_category } ) {
 
 345       my $temp = { ( id => @{$row_cat}[0], ) };
 
 346       push ( @categories, $temp );
 
 350   #my @upload_img = $shop_part->get_images;
 
 351   my $partnumber = $::form->escape($part->partnumber);#don't accept / in partnumber
 
 352   my $stock_status = ($part->onhand ? "instock" : "outofstock");
 
 353   my $status = ($shop_part->active ? "publish" : "private");
 
 354   my $tax_n_price = $shop_part->get_tax_and_price;
 
 355   my $price = $tax_n_price->{price};
 
 356   #my $taxrate = $tax_n_price->{tax};
 
 357   #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
 
 361   if($todo eq "price"){
 
 363       regular_price => $price,
 
 365   }elsif($todo eq "stock"){
 
 367       stock_status => $stock_status,
 
 369   }elsif($todo eq "price_stock"){
 
 371       stock_status => $stock_status,
 
 372       regular_price => $price,
 
 374   }elsif($todo eq "active"){
 
 378   }elsif($todo eq "all"){
 
 379   # mapping  still missing attributes,metatags
 
 382       name => $part->description,
 
 383       stock_status => $stock_status,
 
 384       regular_price => $price,
 
 386       description=> $shop_part->shop_description,
 
 387       short_description=> $shop_part->shop_description,
 
 388       categories => [ @categories ],
 
 389       #tax_class => $tax_class,
 
 393   my $dataString = SL::JSON::to_json(\%shop_data);
 
 394   $dataString    = encode_utf8($dataString);
 
 396   # LWP->post = create || LWP->put = update
 
 397   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
 
 399   if($answer->{success} && scalar @{$answer->{data}}){
 
 401     my $woo_shop_part_id = $answer->{data}[0]->{id};
 
 402     $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
 
 405     $answer = $self->send_request("products", $dataString, "post");
 
 408   # don't know if this is needed
 
 410   #  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in partnumber
 
 411   #  my $imgup      = $self->connector->put($url . "api/generatepartimages/$partnumber?useNumberAsId=true");
 
 414   return $answer->{success};
 
 417 sub get_article_info {
 
 419   my $partnumber = $_[1];
 
 421   $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
 
 422   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
 
 424   $answer->{data} = $answer->{data}[0];
 
 425   #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info ", $answer);
 
 428   if($answer->{success} && scalar @{$answer->{data}}){
 
 429     my $part = $self->map_data_to_part($answer->{data}[0]);
 
 430     #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info part", $part);
 
 438 sub map_data_to_part {
 
 439   my ($self, $part_data) = @_;
 
 441   my %map_part_data =  (
 
 442     #id                 => $part_data->{},
 
 443     partnumber         => $part_data->{sku},
 
 444     description        => $part_data->{name},
 
 445     #listprice          => $part_data->{},
 
 446     #sellprice          => $part_data->{},
 
 447     #lastcost           => $part_data->{},
 
 448     #priceupdate        => $part_data->{},
 
 449     #weight             => $part_data->{},
 
 450     notes              => $part_data->{description},
 
 451     #makemodel          => $part_data->{},
 
 452     #rop                => $part_data->{},
 
 455     #bom                => $part_data->{},
 
 456     #image              => $part_data->{},
 
 457     #drawing            => $part_data->{},
 
 458     #microfiche         => $part_data->{},
 
 459     #partsgroup_id      => $part_data->{},
 
 460     #ve                 => $part_data->{},
 
 461     #gv                 => $part_data->{},
 
 462     #itime              => $part_data->{},
 
 463     #mtime              => $part_data->{},
 
 464     #unit               => $part_data->{},
 
 466     #formel             => $part_data->{},
 
 467     #not_discountable   => $part_data->{},
 
 468     #buchungsgruppen_id => $part_data->{},
 
 469     #payment_id         => $part_data->{},
 
 470     #ean                => $part_data->{},
 
 471     #price_factor_id    => $part_data->{},
 
 472     #onhand             => $part_data->{},
 
 473     #stockable          => $part_data->{},
 
 474     #has_sernumber      => $part_data->{},
 
 475     #warehouse_id       => $part_data->{},
 
 476     #bin_id             => $part_data->{},
 
 477     #df_status_aktuell  => $part_data->{},
 
 478     #df_status_verlauf  => $part_data->{},
 
 479     #active             => $part_data->{},
 
 480     #classification_id  => $part_data->{},
 
 483   return SL::DB::Part->new(%map_part_data);
 
 486 sub map_data_to_shop_part {
 
 487   my ($self, $part_data, $part) = @_;
 
 490   foreach my $row_cat ( @{ $part_data->{categories} } ) {
 
 492     push( @tmp,$row_cat->{id} );
 
 493     push( @tmp,$row_cat->{name} );
 
 494     push( @categories,\@tmp );
 
 496   my %map_shop_part_data =  (
 
 498     shop_id             => $self->config->id,
 
 499     part_id             => $part->id,
 
 500     shop_description    => $part_data->{description},
 
 505     sortorder           => $part_data->{menu_order},
 
 508     shop_category       => \@categories,
 
 509     #active_price_source => ,
 
 510     #metatag_keywords    => ,
 
 511     #metatag_description => ,
 
 513     #shop_versandhinweis => ,
 
 515   return SL::DB::ShopPart->new(%map_shop_part_data);
 
 518   my ($self, $partnumber) = @_;
 
 520   my $dbh       = SL::DB::client;
 
 522   my $number_of_parts = 0;
 
 527     $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
 
 528     $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
 
 531     $answer = $self->send_request("products/", undef , "get");
 
 532     if ($answer->{total_pages} > 1) {
 
 533       my $current_page = 2;
 
 534       while ($current_page <= $answer->{total_pages}) {
 
 535         my $tmp_answer = $self->send_request("products/", undef , "get", "&page=$current_page");
 
 536         foreach my $part (@{$tmp_answer->{data}}) {
 
 537           push @{$answer->{data}} , $part;
 
 544   if($answer->{success} && scalar @{$answer->{data}}){
 
 545     $dbh->with_transaction( sub{
 
 546       foreach my $part_data (@{$answer->{data}}) {
 
 547       unless (!$part_data->{sku} || SL::DB::Manager::Part->get_all_count( query => [ partnumber => $part_data->{sku} ] )) {
 
 548           my $part = $self->map_data_to_part($part_data);
 
 549           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts part ", $part);
 
 551           my $shop_part = $self->map_data_to_shop_part($part_data, $part);
 
 552           #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts shop_part ", $shop_part);
 
 559       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
 
 563       flash_later('error', $::locale->text('Errors: #1', @errors));
 
 566       shop_id          => $self->config->id,
 
 567       shop_description => $self->config->description,
 
 568       number_of_parts => $number_of_parts,
 
 572       shop_id          => $self->config->id,
 
 573       shop_description => $self->config->description,
 
 574       message          => $answer->{message},
 
 577     %fetched_parts = %error_msg;
 
 579   return \%fetched_parts;
 
 585   my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
 
 586   unless($answer->{success}) {
 
 589   my @data = @{$answer->{data}};
 
 590   my %categories = map { ($_->{id} => $_) } @data;
 
 594     my $parent = $categories{$_->{parent}};
 
 596       $parent->{children} ||= [];
 
 597       push @{$parent->{children}},$_;
 
 599       push @categories_tree, $_;
 
 603   return \@categories_tree;
 
 609   my $answer = $self->send_request("system_status");
 
 610   if($answer->{success}) {
 
 611     my $version = $answer->{data}->{environment}->{version};
 
 614       data    => { version => $version },
 
 622 sub set_orderstatus {
 
 623   my ($self,$order_id, $status) = @_;
 
 624   #  if ($status eq "fetched") { $status =  "processing"; }
 
 625   #  if ($status eq "processing") { $status = "completed"; }
 
 626   my %new_status = (status => $status);
 
 627   my $status_json = SL::JSON::to_json( \%new_status);
 
 628   my $answer = $self->send_request("orders/$order_id", $status_json, "put");
 
 629   unless($answer->{success}){
 
 638   my $parameters = $_[2];
 
 640   my $consumer_key = $self->config->login;
 
 641   my $consumer_secret = $self->config->password;
 
 642   my $protocol = $self->config->protocol;
 
 643   my $server = $self->config->server;
 
 644   my $port = $self->config->port;
 
 645   my $path = $self->config->path;
 
 647   return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
 
 653   my $json_data = $_[2];
 
 654   my $method_type = $_[3];
 
 655   my $parameters = $_[4];
 
 657   my $ua = LWP::UserAgent->new;
 
 658   my $url = $self->create_url( $request, $parameters );
 
 661   if( $method_type eq "put" ) {
 
 662     $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
 
 663   } elsif ( $method_type eq "post") {
 
 664     $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
 
 666     $answer = $ua->get($url);
 
 669   my $type = $answer->content_type;
 
 670   my $status_line = $answer->status_line;
 
 673   if($answer->is_success && $type eq 'application/json'){
 
 674     my $data_json = $answer->content;
 
 675     #$main::lxdebug->dump(0, "TST: WooCommerce send_request header ", $answer->header( 'Link'));
 
 676     my $json = SL::JSON::decode_json($data_json);
 
 680       total_pages => $answer->header( 'X-WP-TotalPages'),
 
 681       total_elements => $answer->header( 'X-WP-Total'),
 
 686       data    => { version => $url . ": " . $status_line, data_type => $type },
 
 687       message => "Error: $status_line",
 
 690   #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);