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 $answer= $self->send_request("taxes");
134 unless ($answer->{success}){ return 0;}
135 my %taxes = map { ($_->{id} => $_) } @{ $answer->{data} };
137 my $active_price_source = $self->config->price_source;
139 foreach my $pos(@positions) {
140 my $price = $::form->round_amount($pos->{total},2);
141 my $tax_id = $pos->{taxes}[0]->{id};
142 my $tax_rate = $taxes{ $tax_id }{rate};
143 my %pos_columns = ( description => $pos->{name},
144 partnumber => $pos->{sku}, # sku has to be a valid value in WooCommerce
146 quantity => $pos->{quantity},
147 position => $position,
148 tax_rate => $tax_rate,
149 shop_trans_id => $pos->{product_id},
150 shop_order_id => $id,
151 active_price_source => $active_price_source,
153 #$main::lxdebug->dump(0, "TST: WooCommerce save pos", $pos);
154 #$main::lxdebug->dump(0, "TST: WooCommerce save pos_columns", \%pos_columns);
155 my $pos_insert = SL::DB::ShopOrderItem->new(%pos_columns);
159 $shop_order->positions($position-1);
161 my $customer = $shop_order->get_customer;
164 $shop_order->kivi_customer_id($customer->id);
170 sub map_data_to_shoporder {
171 my ($self, $import) = @_;
173 my $parser = DateTime::Format::Strptime->new( pattern => '%Y-%m-%dT%H:%M:%S',
178 my $shop_id = $self->config->id;
180 # Mapping to table shoporders. See https://woocommerce.github.io/woocommerce-rest-api-docs/?shell#order-properties
182 if ( $import->{shipping}->{address_1} ne "" ) {
183 $d_street = $import->{shipping}->{address_1} . ($import->{shipping}->{address_2} ? " " . $import->{shipping}->{address_2} : "");
185 $d_street = $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : "");
189 billing_firstname => $import->{billing}->{first_name},
190 billing_lastname => $import->{billing}->{last_name},
192 billing_street => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
194 billing_city => $import->{billing}->{city},
197 billing_zipcode => $import->{billing}->{postcode},
198 billing_country => $import->{billing}->{country},
199 billing_email => $import->{billing}->{email},
200 billing_phone => $import->{billing}->{phone},
202 #billing_greeting => "",
205 billing_company => $import->{billing}->{company},
206 #billing_department => "",
210 shop_customer_id => $import->{customer_id},
211 shop_customer_number => $import->{customer_id},
213 remote_ip => $import->{customer_ip_address},
216 shop_customer_comment => $import->{customer_note},
218 #customer_city => "",
219 #customer_company => "",
220 #customer_country => "",
221 #customer_department => "",
222 #customer_email => "",
224 #customer_firstname => "",
225 #customer_greeting => "",
226 #customer_lastname => "",
227 #customer_phone => "",
228 #customer_street => "",
232 delivery_firstname => $import->{shipping}->{first_name} || $import->{billing}->{first_name},
233 delivery_lastname => $import->{shipping}->{last_name} || $import->{billing}->{last_name},
234 delivery_company => $import->{shipping}->{company} || $import->{billing}->{company},
236 delivery_street => $d_street,
237 delivery_city => $import->{shipping}->{city} || $import->{billing}->{city},
239 delivery_zipcode => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
240 delivery_country => $import->{shipping}->{country} || $import->{billing}->{country},
241 #delivery_department => "",
242 #delivery_email => "",
244 #delivery_phone => "",
251 shop_ordernumber => $import->{number},
258 order_date => $parser->parse_datetime($import->{date_created}),
265 shipping_costs => $import->{shipping_total},
267 shipping_costs_net => $import->{shipping_total},
270 amount => $import->{total},
272 netamount => $import->{total} - $import->{total_tax},
274 tax_included => $import->{prices_include_tax} eq "true" ? 1 : 0,
276 # ??? payment_id => $import->{payment_method},
277 #payment_method_title
278 payment_description => $import->{payment}->{payment_method_title},
280 shop_trans_id => $import->{id},
286 host => $import->{_links}->{self}[0]->{href},
288 #sepa_account_holder => "",
292 #shop_c_billing_id => "",
293 #shop_c_billing_number => "",
294 shop_c_delivery_id => $import->{shipping_lines}[0]->{id}, # ???
300 my $shop_order = SL::DB::ShopOrder->new(%columns);
304 #TODO CVARS, tax and images
306 my ($self, $shop_part, $todo) = @_;
308 #shop_part is passed as a param
309 die unless ref($shop_part) eq 'SL::DB::ShopPart';
310 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
315 # ($_->config->name => {
316 # value => $_->value_as_text,
317 # is_valid => $_->is_valid
320 # @{ $part->cvars_by_config }
324 if ($shop_part->shop_category) {
325 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
326 my $temp = { ( id => @{$row_cat}[0], ) };
327 push ( @categories, $temp );
331 #my @upload_img = $shop_part->get_images;
332 my $partnumber = $::form->escape($part->partnumber);#don't accept / in partnumber
333 my $stock_status = ($part->onhand ? "instock" : "outofstock");
334 my $status = ($shop_part->active ? "publish" : "private");
335 my $tax_n_price = $shop_part->get_tax_and_price;
336 my $price = $tax_n_price->{price};
337 #my $taxrate = $tax_n_price->{tax};
338 #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
342 if($todo eq "price"){
344 regular_price => $price,
346 }elsif($todo eq "stock"){
348 stock_status => $stock_status,
350 }elsif($todo eq "price_stock"){
352 stock_status => $stock_status,
353 regular_price => $price,
355 }elsif($todo eq "active"){
359 }elsif($todo eq "all"){
360 # mapping still missing attributes,metatags
363 name => $part->description,
364 stock_status => $stock_status,
365 regular_price => $price,
367 description=> $shop_part->shop_description,
368 short_description=> $shop_part->shop_description,
369 categories => [ @categories ],
370 #tax_class => $tax_class,
374 my $dataString = SL::JSON::to_json(\%shop_data);
375 $dataString = encode_utf8($dataString);
377 # LWP->post = create || LWP->put = update
378 my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
380 if($answer->{success} && scalar @{$answer->{data}}){
382 my $woo_shop_part_id = $answer->{data}[0]->{id};
383 $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
386 $answer = $self->send_request("products", $dataString, "post");
389 # don't know if this is needed
391 # my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in partnumber
392 # my $imgup = $self->connector->put($url . "api/generatepartimages/$partnumber?useNumberAsId=true");
395 return $answer->{success};
398 sub get_article_info {
400 my $partnumber = $_[1];
402 $partnumber = $::form->escape($partnumber);#don't accept / in partnumber
403 my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
405 $answer->{data} = $answer->{data}[0];
406 #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info ", $answer);
409 if($answer->{success} && scalar @{$answer->{data}}){
410 my $part = $self->map_data_to_part($answer->{data}[0]);
411 #$main::lxdebug->dump(0, "TST: WooCommerce get_part_info part", $part);
419 sub map_data_to_part {
420 my ($self, $part_data) = @_;
422 my %map_part_data = (
423 #id => $part_data->{},
424 partnumber => $part_data->{sku},
425 description => $part_data->{name},
426 #listprice => $part_data->{},
427 #sellprice => $part_data->{},
428 #lastcost => $part_data->{},
429 #priceupdate => $part_data->{},
430 #weight => $part_data->{},
431 notes => $part_data->{description},
432 #makemodel => $part_data->{},
433 #rop => $part_data->{},
436 #bom => $part_data->{},
437 #image => $part_data->{},
438 #drawing => $part_data->{},
439 #microfiche => $part_data->{},
440 #partsgroup_id => $part_data->{},
441 #ve => $part_data->{},
442 #gv => $part_data->{},
443 #itime => $part_data->{},
444 #mtime => $part_data->{},
445 #unit => $part_data->{},
447 #formel => $part_data->{},
448 #not_discountable => $part_data->{},
449 #buchungsgruppen_id => $part_data->{},
450 #payment_id => $part_data->{},
451 #ean => $part_data->{},
452 #price_factor_id => $part_data->{},
453 #onhand => $part_data->{},
454 #stockable => $part_data->{},
455 #has_sernumber => $part_data->{},
456 #warehouse_id => $part_data->{},
457 #bin_id => $part_data->{},
458 #df_status_aktuell => $part_data->{},
459 #df_status_verlauf => $part_data->{},
460 #active => $part_data->{},
461 #classification_id => $part_data->{},
464 return SL::DB::Part->new(%map_part_data);
467 sub map_data_to_shop_part {
468 my ($self, $part_data, $part) = @_;
471 foreach my $row_cat ( @{ $part_data->{categories} } ) {
473 push( @tmp,$row_cat->{id} );
474 push( @tmp,$row_cat->{name} );
475 push( @categories,\@tmp );
477 my %map_shop_part_data = (
479 shop_id => $self->config->id,
480 part_id => $part->id,
481 shop_description => $part_data->{description},
486 sortorder => $part_data->{menu_order},
489 shop_category => \@categories,
490 #active_price_source => ,
491 #metatag_keywords => ,
492 #metatag_description => ,
494 #shop_versandhinweis => ,
496 return SL::DB::ShopPart->new(%map_shop_part_data);
499 my ($self, $partnumber) = @_;
501 my $dbh = SL::DB::client;
503 my $number_of_parts = 0;
508 $partnumber = $::form->escape($partnumber);#don't accept / in partnumber
509 $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
512 $answer = $self->send_request("products/", undef , "get");
513 if ($answer->{total_pages} > 1) {
514 my $current_page = 2;
515 while ($current_page <= $answer->{total_pages}) {
516 my $tmp_answer = $self->send_request("products/", undef , "get", "&page=$current_page");
517 foreach my $part (@{$tmp_answer->{data}}) {
518 push @{$answer->{data}} , $part;
525 if($answer->{success} && scalar @{$answer->{data}}){
526 $dbh->with_transaction( sub{
527 foreach my $part_data (@{$answer->{data}}) {
528 unless (!$part_data->{sku} || SL::DB::Manager::Part->get_all_count( query => [ partnumber => $part_data->{sku} ] )) {
529 my $part = $self->map_data_to_part($part_data);
530 #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts part ", $part);
532 my $shop_part = $self->map_data_to_shop_part($part_data, $part);
533 #$main::lxdebug->dump(0, "TST: WooCommerce get_shop_parts shop_part ", $shop_part);
540 push @errors,($::locale->text('Saving failed. Error message from the database: #1', $dbh->error));
544 flash_later('error', $::locale->text('Errors: #1', @errors));
547 shop_id => $self->config->id,
548 shop_description => $self->config->description,
549 number_of_parts => $number_of_parts,
553 shop_id => $self->config->id,
554 shop_description => $self->config->description,
555 message => $answer->{message},
558 %fetched_parts = %error_msg;
560 return \%fetched_parts;
566 my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
567 unless($answer->{success}) {
570 my @data = @{$answer->{data}};
571 my %categories = map { ($_->{id} => $_) } @data;
575 my $parent = $categories{$_->{parent}};
577 $parent->{children} ||= [];
578 push @{$parent->{children}},$_;
580 push @categories_tree, $_;
584 return \@categories_tree;
590 my $answer = $self->send_request("system_status");
591 if($answer->{success}) {
592 my $version = $answer->{data}->{environment}->{version};
595 data => { version => $version },
603 sub set_orderstatus {
604 my ($self,$order_id, $status) = @_;
605 # if ($status eq "fetched") { $status = "processing"; }
606 # if ($status eq "processing") { $status = "completed"; }
607 my %new_status = (status => $status);
608 my $status_json = SL::JSON::to_json( \%new_status);
609 my $answer = $self->send_request("orders/$order_id", $status_json, "put");
610 unless($answer->{success}){
619 my $parameters = $_[2];
621 my $consumer_key = $self->config->login;
622 my $consumer_secret = $self->config->password;
623 my $protocol = $self->config->protocol;
624 my $server = $self->config->server;
625 my $port = $self->config->port;
626 my $path = $self->config->path;
628 return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
634 my $json_data = $_[2];
635 my $method_type = $_[3];
636 my $parameters = $_[4];
638 my $ua = LWP::UserAgent->new;
639 my $url = $self->create_url( $request, $parameters );
642 if( $method_type eq "put" ) {
643 $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
644 } elsif ( $method_type eq "post") {
645 $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
647 $answer = $ua->get($url);
650 my $type = $answer->content_type;
651 my $status_line = $answer->status_line;
654 if($answer->is_success && $type eq 'application/json'){
655 my $data_json = $answer->content;
656 #$main::lxdebug->dump(0, "TST: WooCommerce send_request header ", $answer->header( 'Link'));
657 my $json = SL::JSON::decode_json($data_json);
661 total_pages => $answer->header( 'X-WP-TotalPages'),
662 total_elements => $answer->header( 'X-WP-Total'),
667 data => { version => $url . ": " . $status_line, data_type => $type },
668 message => "Error: $status_line",
671 #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);