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;
 
  12 use SL::DB::PaymentTerm;
 
  16 use SL::Helper::Flash;
 
  17 use Encode qw(encode_utf8);
 
  20   my ($self, $order_id) = @_;
 
  22   my $dbh       = SL::DB::client;
 
  23   my $number_of_orders = 0;
 
  26   my $answer = $self->send_request(
 
  27     "orders/" . $order_id,
 
  32   if($answer->{success}) {
 
  33     my $shoporder = $answer->{data};
 
  35     $dbh->with_transaction( sub{
 
  36         #update status on server
 
  37         $shoporder->{status} = "processing";
 
  38         my $answer = $self->set_orderstatus($shoporder->{id}, "completed");
 
  40           push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
 
  44         unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
 
  48       push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
 
  52       flash_later('error', $::locale->text('Errors: #1', @errors));
 
  56     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
 
  59       shop_id          => $self->config->id,
 
  60       shop_description => $self->config->description,
 
  61       message          => $answer->{message},
 
  64     %fetched_orders = %error_msg;
 
  66   return \%fetched_orders;
 
  72   my $dbh       = SL::DB::client;
 
  73   my $otf              = $self->config->orders_to_fetch || 10;
 
  74   my $number_of_orders = 0;
 
  77   my $answer = $self->send_request(
 
  81     "&per_page=$otf&status=processing&after=2020-12-31T23:59:59&order=asc"
 
  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");
 
  92             push @errors,($::locale->text('Saving failed. Error message from the server: #1', $answer->message));
 
  96           unless ($self->import_data_to_shop_order($shoporder)) { return 0;}
 
 100         push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
 
 104         flash_later('error', $::locale->text('Errors: #1', @errors));
 
 109     %fetched_orders = (shop_description => $self->config->description, number_of_orders => $number_of_orders);
 
 113       shop_id          => $self->config->id,
 
 114       shop_description => $self->config->description,
 
 115       message          => $answer->{message},
 
 118     %fetched_orders = %error_msg;
 
 121   return \%fetched_orders;
 
 124 sub import_data_to_shop_order {
 
 125   my ( $self, $import ) = @_;
 
 126   my $shop_order = $self->map_data_to_shoporder($import);
 
 129   my $id = $shop_order->id;
 
 131   my @positions = sort { Sort::Naturally::ncmp($a->{"sku"}, $b->{"sku"}) } @{ $import->{line_items} };
 
 134   my $active_price_source = $self->config->price_source;
 
 135   my $tax_included = $self->config->pricetype eq 'brutto' ? 1 : 0;
 
 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);
 
 145       $price = $::form->round_amount($price,2);
 
 147     my %pos_columns = ( description          => $pos->{name},
 
 148                         partnumber           => $pos->{sku}, # sku has to be a valid value in WooCommerce
 
 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,
 
 157     my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
 
 161   $shop_order->positions($position-1);
 
 163   if ( $self->config->shipping_costs_parts_id ) {
 
 164     my $shipping_part = SL::DB::Part->find_by( id => $self->config->shipping_costs_parts_id);
 
 166       description    => $import->{data}->{dispatch}->{name},
 
 167       partnumber     => $shipping_part->partnumber,
 
 168       price          => $import->{data}->{invoiceShipping},
 
 170       position       => $position,
 
 172       shop_order_id  => $id,
 
 174     my $shipping_pos_insert = SL::DB::ShopOrderItem->new(%shipping_pos);
 
 175     $shipping_pos_insert->save;
 
 178   my $customer = $shop_order->get_customer;
 
 181     $shop_order->kivi_customer_id($customer->id);
 
 187 sub map_data_to_shoporder {
 
 188   my ($self, $import) = @_;
 
 190   my $parser = DateTime::Format::Strptime->new( pattern   => '%Y-%m-%dT%H:%M:%S',
 
 195   my $shop_id      = $self->config->id;
 
 196   my $tax_included = $self->config->pricetype;
 
 198   # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
 
 200     if ( $import->{shipping}->{address_1} ne "" ) {
 
 201       $d_street = $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : "");
 
 203       $d_street = $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : "");
 
 205   # Mapping Zahlungsmethoden muss an Firmenkonfiguration angepasst werden
 
 206   my %payment_ids_methods = (
 
 207     # woocommerce_payment_method_title => kivitendo_payment_id
 
 209   my $default_payment    = SL::DB::Manager::PaymentTerm->get_first();
 
 210   my $default_payment_id = $default_payment ? $default_payment->id : undef;
 
 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},
 
 216     billing_street         => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
 
 218     billing_city            => $import->{billing}->{city},
 
 221     billing_zipcode         => $import->{billing}->{postcode},
 
 222     billing_country         => $import->{billing}->{country},
 
 223     billing_email           => $import->{billing}->{email},
 
 224     billing_phone           => $import->{billing}->{phone},
 
 226     #billing_greeting        => "",
 
 229     billing_company         => $import->{billing}->{company},
 
 230     #billing_department      => "",
 
 234     shop_customer_id        => $import->{customer_id},
 
 235     shop_customer_number    => $import->{customer_id},
 
 237     remote_ip               => $import->{customer_ip_address},
 
 240     shop_customer_comment   => $import->{customer_note},
 
 242     #customer_city           => "",
 
 243     #customer_company        => "",
 
 244     #customer_country        => "",
 
 245     #customer_department     => "",
 
 246     #customer_email          => "",
 
 248     #customer_firstname      => "",
 
 249     #customer_greeting       => "",
 
 250     #customer_lastname       => "",
 
 251     #customer_phone          => "",
 
 252     #customer_street         => "",
 
 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},
 
 260     delivery_street         => $d_street,
 
 261     delivery_city           => $import->{shipping}->{city} || $import->{billing}->{city},
 
 263     delivery_zipcode        => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
 
 264     delivery_country        => $import->{shipping}->{country} || $import->{billing}->{country},
 
 265     #delivery_department     => "",
 
 266     #delivery_email          => "",
 
 268     #delivery_phone          => "",
 
 275     shop_ordernumber        => $import->{number},
 
 282     order_date              => $parser->parse_datetime($import->{date_created}),
 
 289     shipping_costs          => $import->{shipping_total},
 
 291     shipping_costs_net      => $import->{shipping_total},
 
 294     amount                  => $import->{total},
 
 296     netamount               => $import->{total} - $import->{total_tax},
 
 298     tax_included            => $tax_included,
 
 300     payment_id              => $payment_ids_methods{$import->{payment_method}} || $default_payment_id,
 
 301     #payment_method_title
 
 302     payment_description     => $import->{payment_method_title},
 
 304     shop_trans_id           => $import->{id},
 
 310     host                    => $import->{_links}->{self}[0]->{href},
 
 312     #sepa_account_holder     => "",
 
 316     #shop_c_billing_id       => "",
 
 317     #shop_c_billing_number   => "",
 
 318     shop_c_delivery_id      => $import->{shipping_lines}[0]->{id}, # ???
 
 324   my $shop_order = SL::DB::ShopOrder->new(%columns);
 
 328 #TODO CVARS, tax and images
 
 330   my ($self, $shop_part, $todo) = @_;
 
 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;
 
 339   #    ($_->config->name => {
 
 340   #      value => $_->value_as_text,
 
 341   #      is_valid => $_->is_valid
 
 344   #  @{ $part->cvars_by_config }
 
 348   foreach my $row_cat ( @{ $shop_part->shop_category } ) {
 
 349     my $temp = { ( id => @{$row_cat}[0], ) };
 
 350     push ( @categories, $temp );
 
 353   #my @upload_img = $shop_part->get_images;
 
 354   my $partnumber = $::form->escape($part->partnumber);#don't accept / in articlenumber
 
 355   my $stock_status = ($part->onhand ? "instock" : "outofstock");
 
 356   my $status = ($shop_part->active ? "publish" : "private");
 
 357   my $tax_n_price = $shop_part->get_tax_and_price;
 
 358   my $price = $tax_n_price->{price};
 
 359   #my $taxrate = $tax_n_price->{tax};
 
 360   #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
 
 364   if($todo eq "price"){
 
 366       regular_price => $price,
 
 368   }elsif($todo eq "stock"){
 
 370       stock_status => $stock_status,
 
 372   }elsif($todo eq "price_stock"){
 
 374       stock_status => $stock_status,
 
 375       regular_price => $price,
 
 377   }elsif($todo eq "active"){
 
 381   }elsif($todo eq "all"){
 
 382   # mapping  still missing attributes,metatags
 
 385       name => $part->description,
 
 386       stock_status => $stock_status,
 
 387       regular_price => $price,
 
 389       description=> $shop_part->shop_description,
 
 390       short_description=> $shop_part->shop_description,
 
 391       categories => [ @categories ],
 
 392       #tax_class => $tax_class,
 
 396   my $dataString = SL::JSON::to_json(\%shop_data);
 
 397   $dataString    = encode_utf8($dataString);
 
 399   # LWP->post = create || LWP->put = update
 
 400   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
 
 402   if($answer->{success} && scalar @{$answer->{data}}){
 
 404     my $woo_shop_part_id = $answer->{data}[0]->{id};
 
 405     $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
 
 408     $answer = $self->send_request("products", $dataString, "post");
 
 411   # don't know if this is needed
 
 413   #  my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
 
 414   #  my $imgup      = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
 
 417   return $answer->{success};
 
 422   my $partnumber = $_[1];
 
 424   $partnumber   = $::form->escape($partnumber);#don't accept / in partnumber
 
 425   my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
 
 427   if($answer->{success} && scalar @{$answer->{data}}){
 
 428     my $article = $answer->{data}[0];
 
 439   my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
 
 440   unless($answer->{success}) {
 
 443   my @data = @{$answer->{data}};
 
 444   my %categories = map { ($_->{id} => $_) } @data;
 
 448     my $parent = $categories{$_->{parent}};
 
 450       $parent->{children} ||= [];
 
 451       push @{$parent->{children}},$_;
 
 453       push @categories_tree, $_;
 
 457   return \@categories_tree;
 
 463   my $answer = $self->send_request("system_status");
 
 464   if($answer->{success}) {
 
 465     my $version = $answer->{data}->{environment}->{version};
 
 468       data    => { version => $version },
 
 476 sub set_orderstatus {
 
 477   my ($self,$order_id, $status) = @_;
 
 478   #  if ($status eq "fetched") { $status =  "processing"; }
 
 479   #  if ($status eq "processing") { $status = "completed"; }
 
 480   my %new_status = (status => $status);
 
 481   my $status_json = SL::JSON::to_json( \%new_status);
 
 482   my $answer = $self->send_request("orders/$order_id", $status_json, "put");
 
 483   unless($answer->{success}){
 
 492   my $parameters = $_[2];
 
 494   my $consumer_key = $self->config->login;
 
 495   my $consumer_secret = $self->config->password;
 
 496   my $protocol = $self->config->protocol;
 
 497   my $server = $self->config->server;
 
 498   my $port = $self->config->port;
 
 499   my $path = $self->config->path;
 
 501   return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
 
 507   my $json_data = $_[2];
 
 508   my $method_type = $_[3];
 
 509   my $parameters = $_[4];
 
 511   my $ua = LWP::UserAgent->new;
 
 512   my $url = $self->create_url( $request, $parameters );
 
 515   if( $method_type eq "put" ) {
 
 516     $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
 
 517   } elsif ( $method_type eq "post") {
 
 518     $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
 
 520     $answer = $ua->get($url);
 
 523   my $type = $answer->content_type;
 
 524   my $status_line = $answer->status_line;
 
 527   if($answer->is_success && $type eq 'application/json'){
 
 528     my $data_json = $answer->content;
 
 529     my $json = SL::JSON::decode_json($data_json);
 
 537       data    => { version => $url . ": " . $status_line, data_type => $type },
 
 538       message => "Error: $status_line",
 
 541   #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);