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_id = SL::DB::Manager::PaymentTerm->get_first()->id || undef;
211 #billing Shop can have different billing addresses, and may have 1 customer_address
212 billing_firstname => $import->{billing}->{first_name},
213 billing_lastname => $import->{billing}->{last_name},
215 billing_street => $import->{billing}->{address_1} . ($import->{billing}->{address_2} ? " " . $import->{billing}->{address_2} : ""),
217 billing_city => $import->{billing}->{city},
220 billing_zipcode => $import->{billing}->{postcode},
221 billing_country => $import->{billing}->{country},
222 billing_email => $import->{billing}->{email},
223 billing_phone => $import->{billing}->{phone},
225 #billing_greeting => "",
228 billing_company => $import->{billing}->{company},
229 #billing_department => "",
233 shop_customer_id => $import->{customer_id},
234 shop_customer_number => $import->{customer_id},
236 remote_ip => $import->{customer_ip_address},
239 shop_customer_comment => $import->{customer_note},
241 #customer_city => "",
242 #customer_company => "",
243 #customer_country => "",
244 #customer_department => "",
245 #customer_email => "",
247 #customer_firstname => "",
248 #customer_greeting => "",
249 #customer_lastname => "",
250 #customer_phone => "",
251 #customer_street => "",
255 delivery_firstname => $import->{shipping}->{first_name} || $import->{billing}->{first_name},
256 delivery_lastname => $import->{shipping}->{last_name} || $import->{billing}->{last_name},
257 delivery_company => $import->{shipping}->{company} || $import->{billing}->{company},
259 delivery_street => $d_street,
260 delivery_city => $import->{shipping}->{city} || $import->{billing}->{city},
262 delivery_zipcode => $import->{shipping}->{postcode} || $import->{billing}->{postcode},
263 delivery_country => $import->{shipping}->{country} || $import->{billing}->{country},
264 #delivery_department => "",
265 #delivery_email => "",
267 #delivery_phone => "",
274 shop_ordernumber => $import->{number},
281 order_date => $parser->parse_datetime($import->{date_created}),
288 shipping_costs => $import->{shipping_total},
290 shipping_costs_net => $import->{shipping_total},
293 amount => $import->{total},
295 netamount => $import->{total} - $import->{total_tax},
297 tax_included => $tax_included,
299 payment_id => $payment_ids_methods{$import->{payment_method}} || $default_payment_id,
300 #payment_method_title
301 payment_description => $import->{payment_method_title},
303 shop_trans_id => $import->{id},
309 host => $import->{_links}->{self}[0]->{href},
311 #sepa_account_holder => "",
315 #shop_c_billing_id => "",
316 #shop_c_billing_number => "",
317 shop_c_delivery_id => $import->{shipping_lines}[0]->{id}, # ???
323 my $shop_order = SL::DB::ShopOrder->new(%columns);
327 #TODO CVARS, tax and images
329 my ($self, $shop_part, $todo) = @_;
331 #shop_part is passed as a param
332 die unless ref($shop_part) eq 'SL::DB::ShopPart';
333 my $part = SL::DB::Part->new(id => $shop_part->part_id)->load;
338 # ($_->config->name => {
339 # value => $_->value_as_text,
340 # is_valid => $_->is_valid
343 # @{ $part->cvars_by_config }
347 foreach my $row_cat ( @{ $shop_part->shop_category } ) {
348 my $temp = { ( id => @{$row_cat}[0], ) };
349 push ( @categories, $temp );
352 #my @upload_img = $shop_part->get_images;
353 my $partnumber = $::form->escape($part->partnumber);#don't accept / in articlenumber
354 my $stock_status = ($part->onhand ? "instock" : "outofstock");
355 my $status = ($shop_part->active ? "publish" : "private");
356 my $tax_n_price = $shop_part->get_tax_and_price;
357 my $price = $tax_n_price->{price};
358 #my $taxrate = $tax_n_price->{tax};
359 #my $tax_class = ($taxrate >= 16 ? "standard" : "reduzierter-preis");
363 if($todo eq "price"){
365 regular_price => $price,
367 }elsif($todo eq "stock"){
369 stock_status => $stock_status,
371 }elsif($todo eq "price_stock"){
373 stock_status => $stock_status,
374 regular_price => $price,
376 }elsif($todo eq "active"){
380 }elsif($todo eq "all"){
381 # mapping still missing attributes,metatags
384 name => $part->description,
385 stock_status => $stock_status,
386 regular_price => $price,
388 description=> $shop_part->shop_description,
389 short_description=> $shop_part->shop_description,
390 categories => [ @categories ],
391 #tax_class => $tax_class,
395 my $dataString = SL::JSON::to_json(\%shop_data);
396 $dataString = encode_utf8($dataString);
398 # LWP->post = create || LWP->put = update
399 my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
401 if($answer->{success} && scalar @{$answer->{data}}){
403 my $woo_shop_part_id = $answer->{data}[0]->{id};
404 $answer = $self->send_request("products/$woo_shop_part_id", $dataString, "put");
407 $answer = $self->send_request("products", $dataString, "post");
410 # don't know if this is needed
412 # my $partnumber = $::form->escape($part->partnumber);#shopware don't accept / in articlenumber
413 # my $imgup = $self->connector->put($url . "api/generatearticleimages/$partnumber?useNumberAsId=true");
416 return $answer->{success};
421 my $partnumber = $_[1];
423 $partnumber = $::form->escape($partnumber);#don't accept / in partnumber
424 my $answer = $self->send_request("products/", undef , "get" , "&sku=$partnumber");
426 if($answer->{success} && scalar @{$answer->{data}}){
427 my $article = $answer->{data}[0];
438 my $answer = $self->send_request("products/categories",undef,"get","&per_page=100");
439 unless($answer->{success}) {
442 my @data = @{$answer->{data}};
443 my %categories = map { ($_->{id} => $_) } @data;
447 my $parent = $categories{$_->{parent}};
449 $parent->{children} ||= [];
450 push @{$parent->{children}},$_;
452 push @categories_tree, $_;
456 return \@categories_tree;
462 my $answer = $self->send_request("system_status");
463 if($answer->{success}) {
464 my $version = $answer->{data}->{environment}->{version};
467 data => { version => $version },
475 sub set_orderstatus {
476 my ($self,$order_id, $status) = @_;
477 # if ($status eq "fetched") { $status = "processing"; }
478 # if ($status eq "processing") { $status = "completed"; }
479 my %new_status = (status => $status);
480 my $status_json = SL::JSON::to_json( \%new_status);
481 my $answer = $self->send_request("orders/$order_id", $status_json, "put");
482 unless($answer->{success}){
491 my $parameters = $_[2];
493 my $consumer_key = $self->config->login;
494 my $consumer_secret = $self->config->password;
495 my $protocol = $self->config->protocol;
496 my $server = $self->config->server;
497 my $port = $self->config->port;
498 my $path = $self->config->path;
500 return $protocol . "://". $server . ":" . $port . $path . $request . "?consumer_key=" . $consumer_key . "&consumer_secret=" . $consumer_secret . $parameters;
506 my $json_data = $_[2];
507 my $method_type = $_[3];
508 my $parameters = $_[4];
510 my $ua = LWP::UserAgent->new;
511 my $url = $self->create_url( $request, $parameters );
514 if( $method_type eq "put" ) {
515 $answer = $ua->put($url, "Content-Type" => "application/json", Content => $json_data);
516 } elsif ( $method_type eq "post") {
517 $answer = $ua->post($url, "Content-Type" => "application/json", Content => $json_data);
519 $answer = $ua->get($url);
522 my $type = $answer->content_type;
523 my $status_line = $answer->status_line;
526 if($answer->is_success && $type eq 'application/json'){
527 my $data_json = $answer->content;
528 my $json = SL::JSON::decode_json($data_json);
536 data => { version => $url . ": " . $status_line, data_type => $type },
537 message => "Error: $status_line",
540 #$main::lxdebug->dump(0, "TST: WooCommerce send_request return ", \%return);