ShopConnector get_part in WooCommerce implemented
[kivitendo-erp.git] / SL / Controller / ShopPart.pm
1 package SL::Controller::ShopPart;
2
3 use strict;
4
5 use parent qw(SL::Controller::Base);
6
7 use SL::BackgroundJob::ShopPartMassUpload;
8 use SL::System::TaskServer;
9 use Data::Dumper;
10 use SL::Locale::String qw(t8);
11 use SL::DB::ShopPart;
12 use SL::DB::Shop;
13 use SL::DB::File;
14 use SL::DB::ShopImage;
15 use SL::DB::Default;
16 use SL::Helper::Flash;
17 use SL::Controller::Helper::ParseFilter;
18 use MIME::Base64;
19
20 use Rose::Object::MakeMethods::Generic
21 (
22    scalar                 => [ qw(price_sources) ],
23   'scalar --get_set_init' => [ qw(shop_part file shops) ],
24 );
25
26 __PACKAGE__->run_before('check_auth');
27 __PACKAGE__->run_before('add_javascripts', only => [ qw(edit_popup list_articles) ]);
28 __PACKAGE__->run_before('load_pricesources',    only => [ qw(create_or_edit_popup) ]);
29
30 #
31 # actions
32 #
33
34 sub action_create_or_edit_popup {
35   my ($self) = @_;
36
37   $self->render_shop_part_edit_dialog();
38 }
39
40 sub action_update_shop {
41   my ($self, %params) = @_;
42
43   my $shop_part = SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
44   die unless $shop_part;
45
46   require SL::Shop;
47   my $shop = SL::Shop->new( config => $shop_part->shop );
48
49   my $connect = $shop->check_connectivity;
50   if($connect->{success}){
51     my $return    = $shop->connector->update_part($self->shop_part, 'all');
52
53     # the connector deals with parsing/result verification, just needs to return success or failure
54     if ( $return == 1 ) {
55       my $now = DateTime->now;
56       my $attributes->{last_update} = $now;
57       $self->shop_part->assign_attributes(%{ $attributes });
58       $self->shop_part->save;
59       $self->js->html('#shop_part_last_update_' . $shop_part->id, $now->to_kivitendo('precision' => 'minute'))
60              ->flash('info', t8("Updated part [#1] in shop [#2] at #3", $shop_part->part->displayable_name, $shop_part->shop->description, $now->to_kivitendo('precision' => 'minute') ) )
61              ->render;
62     } else {
63       $self->js->flash('error', t8('The shop part wasn\'t updated.'))->render;
64     }
65   }else{
66     $self->js->flash('error', t8('The shop part wasn\'t updated. #1', $connect->{data}->{version}))->render;
67   }
68
69
70 }
71
72 sub action_show_files {
73   my ($self) = @_;
74
75   my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $::form->{id}, ], with_objects => 'file', sort_by => 'position' );
76
77   $self->render('shop_part/_list_images', { header => 0 }, IMAGES => $images);
78 }
79
80 sub action_ajax_delete_file {
81   my ( $self ) = @_;
82   $self->file->delete;
83
84   $self->js
85     ->run('kivi.ShopPart.show_images',$self->file->object_id)
86     ->render();
87 }
88
89 sub action_get_shop_parts {
90   my ( $self ) = @_;
91   $main::lxdebug->dump(0, "TST: ShopPart get_shop_parts form", $::form);
92   my $parts_fetched;
93   my $new_parts;
94
95   my $type = $::form->{type};
96   if ( $type eq "get_one" ) {
97     my $shop_id = $::form->{shop_id};
98     my $part_number = $::form->{part_number};
99
100     if ( $shop_id && $part_number ) {
101       my $shop_config = SL::DB::Manager::Shop->get_first(query => [ id => $shop_id, obsolete => 0 ]);
102       my $shop = SL::Shop->new( config => $shop_config );
103       unless ( SL::DB::Manager::Part->get_all_count( query => [ partnumber => $part_number ] )) {
104         $new_parts = $shop->connector->get_shop_parts($part_number);
105         push @{ $parts_fetched }, $new_parts ;
106       } else {
107         flash_later('error', t8('From shop "#1" :  Number: #2 #3 ', $shop->config->description, $part_number, t8('Partnumber is already exist')));
108       }
109     } else {
110         flash_later('error', t8('Shop or partnumber not selected.'));
111     }
112   } elsif ( $type eq "get_new" ) {
113     my $active_shops = SL::DB::Manager::Shop->get_all(query => [ obsolete => 0 ]);
114     foreach my $shop_config ( @{ $active_shops } ) {
115       my $shop = SL::Shop->new( config => $shop_config );
116
117       $new_parts = $shop->connector->get_shop_parts;
118       push @{ $parts_fetched }, $new_parts ;
119     }
120   }
121
122   foreach my $shop_fetched(@{ $parts_fetched }) {
123     if($shop_fetched->{error}){
124       flash_later('error', t8('From shop "#1" :  #2 ', $shop_fetched->{shop_description}, $shop_fetched->{message},));
125     }else{
126       flash_later('info', t8('From shop #1 :  #2 parts have been imported.', $shop_fetched->{description}, $shop_fetched->{number_of_parts},));
127     }
128   }
129
130   $self->redirect_to(controller => "ShopPart", action => 'list_articles');
131 }
132
133 sub action_get_categories {
134   my ($self) = @_;
135
136   require SL::Shop;
137   my $shop = SL::Shop->new( config => $self->shop_part->shop );
138
139   my $connect = $shop->check_connectivity;
140   if($connect->{success}){
141     my $categories = $shop->connector->get_categories;
142
143     $self->js
144       ->run(
145         'kivi.ShopPart.shop_part_dialog',
146         t8('Shopcategories'),
147         $self->render('shop_part/categories', { output => 0 }, CATEGORIES => $categories )
148       )
149       ->reinit_widgets;
150       $self->js->render;
151   }else{
152     $self->js->flash('error', t8('Can\'t connect to shop. #1', $connect->{data}->{version}))->render;
153   }
154
155 }
156
157 sub action_show_price_n_pricesource {
158   my ($self) = @_;
159
160   my ( $price, $price_src_str ) = $self->get_price_n_pricesource($::form->{pricesource});
161
162   if( $price_src_str eq 'sellprice'){
163     $price_src_str = t8('Sellprice');
164   }elsif( $price_src_str eq 'listprice'){
165     $price_src_str = t8('Listprice');
166   }elsif( $price_src_str eq 'lastcost'){
167     $price_src_str = t8('Lastcost');
168   }
169   $self->js->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
170            ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
171            ->render;
172 }
173
174 sub action_show_stock {
175   my ($self) = @_;
176   my ( $stock_local, $stock_onlineshop, $active_online );
177
178   require SL::Shop;
179   my $shop = SL::Shop->new( config => $self->shop_part->shop );
180
181   if($self->shop_part->last_update) {
182     my $shop_article = $shop->connector->get_article_info($self->shop_part->part->partnumber);
183     $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
184     $active_online = $shop_article->{data}->{active};
185   }
186
187   $stock_local = $self->shop_part->part->onhand;
188
189   $self->js->html('#stock_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$stock_local,0)."/".$::form->format_amount(\%::myconfig,$stock_onlineshop,0))
190            ->html('#toogle_' . $self->shop_part->id,$active_online)
191            ->render;
192 }
193
194 sub action_get_n_write_categories {
195   my ($self) = @_;
196
197   my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
198   foreach my $part(@shop_parts){
199
200     my $shop_part = SL::DB::Manager::ShopPart->get_all( where => [id => $part], with_objects => ['part', 'shop'])->[0];
201     require SL::DB::Shop;
202     my $shop = SL::Shop->new( config => $shop_part->shop );
203     my $online_article = $shop->connector->get_article_info($shop_part->part->partnumber);
204     my $online_cat = $online_article->{data}->{categories};
205     my @cat = ();
206     for(keys %$online_cat){
207       my @cattmp;
208       push @cattmp,$online_cat->{$_}->{id};
209       push @cattmp,$online_cat->{$_}->{name};
210       push @cat,\@cattmp;
211     }
212     my $attributes->{shop_category} = \@cat;
213     my $active->{active} = $online_article->{data}->{active};
214     $shop_part->assign_attributes(%{$attributes}, %{$active});
215     $shop_part->save;
216   }
217   $self->redirect_to( action => 'list_articles' );
218 }
219
220 sub action_save_categories {
221   my ($self) = @_;
222
223   my @categories =  @{ $::form->{categories} || [] };
224
225     my @cat = ();
226     foreach my $cat ( @categories) {
227       my @cattmp;
228       push( @cattmp,$cat );
229       push( @cattmp,$::form->{"cat_id_${cat}"} );
230       push( @cat,\@cattmp );
231     }
232
233   my $categories->{shop_category} = \@cat;
234
235   my $params = delete($::form->{shop_part}) || { };
236
237   $self->shop_part->assign_attributes(%{ $params });
238   $self->shop_part->assign_attributes(%{ $categories });
239
240   $self->shop_part->save;
241
242   flash('info', t8('The categories has been saved.'));
243
244   $self->js->run('kivi.ShopPart.close_dialog')
245            ->flash('info', t8("Updated categories"))
246            ->render;
247 }
248
249 sub action_reorder {
250   my ($self) = @_;
251   require SL::DB::ShopImage;
252   SL::DB::ShopImage->reorder_list(@{ $::form->{image_id} || [] });
253
254   $self->render(\'', { type => 'json' });
255 }
256
257 sub action_list_articles {
258   my ($self) = @_;
259
260   my %filter      = ($::form->{filter} ? parse_filter($::form->{filter}) : query => [ 'shop.obsolete' => 0 ]);
261   my $sort_by     = $::form->{sort_by} ? $::form->{sort_by} : 'part.partnumber';
262   $sort_by .=$::form->{sort_dir} ? ' DESC' : ' ASC';
263
264   my $articles = SL::DB::Manager::ShopPart->get_all( %filter ,with_objects => [ 'part','shop' ], sort_by => $sort_by );
265
266   foreach my $article (@{ $articles}) {
267     my $images = SL::DB::Manager::ShopImage->get_all_count( where => [ 'files.object_id' => $article->part->id, ], with_objects => 'file', sort_by => 'position' );
268     $article->{images} = $images;
269   }
270
271   $self->_setup_list_action_bar;
272
273   $self->render('shop_part/_list_articles', title => t8('Webshops articles'), SHOP_PARTS => $articles);
274 }
275
276 sub action_upload_status {
277   my ($self) = @_;
278   my $job     = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
279   my $html    = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
280
281   $self->js->html('#status_mass_upload', $html);
282   $self->js->run('kivi.ShopPart.massUploadFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::ShopPartMassUpload->DONE();
283   $self->js->render;
284 }
285
286 sub action_mass_upload {
287   my ($self) = @_;
288
289   my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
290
291   my $job = SL::DB::BackgroundJob->new(
292         type                 => 'once',
293         active               => 1,
294         package_name         => 'ShopPartMassUpload',
295         )->set_data(
296         shop_part_record_ids => [ @shop_parts ],
297         todo                 => $::form->{upload_todo},
298         status               => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
299         conversation         => [ ],
300         num_uploaded         => 0,
301    )->update_next_run_at;
302
303    SL::System::TaskServer->new->wake_up;
304
305    my $html = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
306
307    $self->js
308       ->html('#status_mass_upload', $html)
309       ->run('kivi.ShopPart.massUploadStarted')
310       ->render;
311 }
312
313 sub action_update {
314   my ($self) = @_;
315
316   $self->create_or_update;
317 }
318
319 sub render_shop_part_edit_dialog {
320   my ($self) = @_;
321
322   $self->js
323     ->run(
324       'kivi.ShopPart.shop_part_dialog',
325       t8('Shop part'),
326       $self->render('shop_part/edit', { output => 0 })
327     )
328     ->reinit_widgets;
329
330   $self->js->render;
331 }
332
333 sub create_or_update {
334   my ($self) = @_;
335
336   my $is_new = !$self->shop_part->id;
337
338   my $params = delete($::form->{shop_part}) || { };
339
340   $self->shop_part->assign_attributes(%{ $params });
341
342   $self->shop_part->save;
343
344   my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
345 if(!$is_new){
346   flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
347   $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
348            ->html('#shop_part_active_' . $self->shop_part->id, $self->shop_part->active)
349            ->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
350            ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
351            ->run('kivi.ShopPart.close_dialog')
352            ->flash('info', t8("Updated shop part"))
353            ->render;
354          }else{
355     $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->shop_part->part_id);
356   }
357 }
358
359 #
360 # internal stuff
361 #
362 sub add_javascripts  {
363   $::request->{layout}->add_javascripts(qw(kivi.ShopPart.js));
364 }
365
366 sub load_pricesources {
367   my ($self) = @_;
368
369   my $pricesources;
370   push( @{ $pricesources } , { id => "master_data/sellprice", name => t8("Master Data")." - ".t8("Sellprice") },
371                              { id => "master_data/listprice", name => t8("Master Data")." - ".t8("Listprice") },
372                              { id => "master_data/lastcost",  name => t8("Master Data")." - ".t8("Lastcost") }
373                              );
374   my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
375   foreach my $pg ( @$pricegroups ) {
376     push( @{ $pricesources } , { id => "pricegroup/".$pg->id, name => t8("Pricegroup") . " - " . $pg->pricegroup} );
377   };
378
379   $self->price_sources( $pricesources );
380 }
381
382 sub get_price_n_pricesource {
383   my ($self,$pricesource) = @_;
384
385   my ( $price_src_str, $price_src_id ) = split(/\//,$pricesource);
386
387   require SL::DB::Pricegroup;
388   require SL::DB::Part;
389   my $price;
390   if ($price_src_str eq "master_data") {
391     my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id );
392     $price         = $part->$price_src_id;
393     $price_src_str = $price_src_id;
394     }else{
395     my $part       = SL::DB::Manager::Part->get_all( where => [id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id], with_objects => ['prices'],limit => 1)->[0];
396     #my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id );
397     my $pricegrp   = SL::DB::Manager::Pricegroup->find_by( id => $price_src_id )->pricegroup;
398     $price         = $part->prices->[0]->price;
399     $price_src_str = $pricegrp;
400   }
401   return($price,$price_src_str);
402 }
403
404 sub _setup_list_action_bar {
405   my ($self) = @_;
406
407   for my $bar ($::request->layout->get('actionbar')) {
408     $bar->add(
409         action => [
410           t8('Search'),
411           submit    => [ '#shop_part_filter', { action => "ShopPart/list_articles" } ],
412         ],
413         action => [
414           t8('Get one part'),
415           call    => [ 'kivi.ShopPart.get_shop_parts_one_setup' ],
416           tooltip => t8('Get one part by partnumber'),
417         ],
418         action => [
419           t8('Get new parts'),
420           call    => [ 'kivi.ShopPart.get_shop_parts_new' ],
421           tooltip => t8('Get all new parts'),
422         ],
423     );
424   }
425 }
426
427 sub check_auth {
428   $::auth->assert('shop_part_edit');
429 }
430
431 sub init_shop_part {
432   if ($::form->{shop_part_id}) {
433     SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
434   } else {
435     SL::DB::ShopPart->new(shop_id => $::form->{shop_id}, part_id => $::form->{part_id});
436   };
437 }
438
439 sub init_file {
440   my $file = $::form->{id} ? SL::DB::File->new(id => $::form->{id})->load : SL::DB::File->new;
441   return $file;
442 }
443
444 sub init_shops {
445   SL::DB::Shop->shops_dd;
446 }
447
448 1;
449
450 __END__
451
452 =encoding utf-8
453
454
455 =head1 NAME
456
457 SL::Controller::ShopPart - Controller for managing ShopParts
458
459 =head1 SYNOPSIS
460
461 ShopParts are configured in a tab of the corresponding part.
462
463 =head1 ACTIONS
464
465 =over 4
466
467 =item C<action_update_shop>
468
469 To be called from the "Update" button of the shoppart, for manually syncing/upload one part with its shop. Calls some ClientJS functions to modifiy original page.
470
471 =item C<action_show_files>
472
473
474
475 =item C<action_ajax_delete_file>
476
477
478
479 =item C<action_get_categories>
480
481
482
483 =item C<action_show_price_n_pricesource>
484
485
486
487 =item C<action_show_stock>
488
489
490
491 =item C<action_get_n_write_categories>
492
493 Can be used to sync the categories of a shoppart with the categories from online.
494
495 =item C<action_save_categories>
496
497 The ShopwareConnector works with the CategoryID @categories[x][0] in others/new Connectors it must be tested
498 Each assigned categorie is saved with id,categorie_name an multidimensional array and could be expanded with categoriepath or what is needed
499
500 =item C<action_reorder>
501
502
503
504 =item C<action_upload_status>
505
506
507
508 =item C<action_mass_upload>
509
510
511
512 =item C<action_update>
513
514
515
516 =item C<create_or_update>
517
518
519
520 =item C<render_shop_part_edit_dialog>
521
522 when self->shop_part is called in template, it will be an existing shop_part with id,
523 or a new shop_part with only part_id and shop_id set
524
525 =item C<add_javascripts>
526
527
528 =item C<load_pricesources>
529
530 the price sources to use for the article: sellprice, lastcost,
531 listprice, or one of the pricegroups. It overwrites the default pricesource from the shopconfig.
532 TODO: implement valid pricerules for the article
533
534 =item C<get_price_n_pricesource>
535
536
537 =item C<check_auth>
538
539
540 =item C<init_shop_part>
541
542
543 =item C<init_file>
544
545
546 =item C<init_shops>
547
548 data for drop down filter options
549
550 =back
551
552 =head1 TODO
553
554 CheckAuth
555 Pricesrules, pricessources aren't fully implemented yet.
556
557 =head1 AUTHORS
558
559 G. Richardson E<lt>information@kivitendo-premium.deE<gt>
560 W. Hahn E<lt>wh@futureworldsearch.netE<gt>
561
562 =cut