6ab65076c702471c8a54ff6883f0ee3ed265c644
[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_categories {
90   my ($self) = @_;
91
92   require SL::Shop;
93   my $shop = SL::Shop->new( config => $self->shop_part->shop );
94
95   my $connect = $shop->check_connectivity;
96   if($connect->{success}){
97     my $categories = $shop->connector->get_categories;
98
99     $self->js
100       ->run(
101         'kivi.ShopPart.shop_part_dialog',
102         t8('Shopcategories'),
103         $self->render('shop_part/categories', { output => 0 }, CATEGORIES => $categories )
104       )
105       ->reinit_widgets;
106       $self->js->render;
107   }else{
108     $self->js->flash('error', t8('Can\'t connect to shop. #1', $connect->{data}->{version}))->render;
109   }
110
111 }
112
113 sub action_show_price_n_pricesource {
114   my ($self) = @_;
115
116   my ( $price, $price_src_str ) = $self->get_price_n_pricesource($::form->{pricesource});
117
118   if( $price_src_str eq 'sellprice'){
119     $price_src_str = t8('Sellprice');
120   }elsif( $price_src_str eq 'listprice'){
121     $price_src_str = t8('Listprice');
122   }elsif( $price_src_str eq 'lastcost'){
123     $price_src_str = t8('Lastcost');
124   }
125   $self->js->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
126            ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
127            ->render;
128 }
129
130 sub action_show_stock {
131   my ($self) = @_;
132   my ( $stock_local, $stock_onlineshop, $active_online );
133
134   require SL::Shop;
135   my $shop = SL::Shop->new( config => $self->shop_part->shop );
136
137   if($self->shop_part->last_update) {
138     my $shop_article = $shop->connector->get_article($self->shop_part->part->partnumber);
139     $stock_onlineshop = $shop_article->{data}->{mainDetail}->{inStock};
140     $active_online = $shop_article->{data}->{active};
141   }
142
143   $stock_local = $self->shop_part->part->onhand;
144
145   $self->js->html('#stock_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$stock_local,0)."/".$::form->format_amount(\%::myconfig,$stock_onlineshop,0))
146            ->html('#toogle_' . $self->shop_part->id,$active_online)
147            ->render;
148 }
149
150 sub action_get_n_write_categories {
151   my ($self) = @_;
152
153   my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
154   foreach my $part(@shop_parts){
155
156     my $shop_part = SL::DB::Manager::ShopPart->get_all( where => [id => $part], with_objects => ['part', 'shop'])->[0];
157     require SL::DB::Shop;
158     my $shop = SL::Shop->new( config => $shop_part->shop );
159     my $online_article = $shop->connector->get_article($shop_part->part->partnumber);
160     my $online_cat = $online_article->{data}->{categories};
161     my @cat = ();
162     for(keys %$online_cat){
163       my @cattmp;
164       push @cattmp,$online_cat->{$_}->{id};
165       push @cattmp,$online_cat->{$_}->{name};
166       push @cat,\@cattmp;
167     }
168     my $attributes->{shop_category} = \@cat;
169     my $active->{active} = $online_article->{data}->{active};
170     $shop_part->assign_attributes(%{$attributes}, %{$active});
171     $shop_part->save;
172   }
173   $self->redirect_to( action => 'list_articles' );
174 }
175
176 sub action_save_categories {
177   my ($self) = @_;
178
179   my @categories =  @{ $::form->{categories} || [] };
180
181     my @cat = ();
182     foreach my $cat ( @categories) {
183       my @cattmp;
184       push( @cattmp,$cat );
185       push( @cattmp,$::form->{"cat_id_${cat}"} );
186       push( @cat,\@cattmp );
187     }
188
189   my $categories->{shop_category} = \@cat;
190
191   my $params = delete($::form->{shop_part}) || { };
192
193   $self->shop_part->assign_attributes(%{ $params });
194   $self->shop_part->assign_attributes(%{ $categories });
195
196   $self->shop_part->save;
197
198   flash('info', t8('The categories has been saved.'));
199
200   $self->js->run('kivi.ShopPart.close_dialog')
201            ->flash('info', t8("Updated categories"))
202            ->render;
203 }
204
205 sub action_reorder {
206   my ($self) = @_;
207   require SL::DB::ShopImage;
208   SL::DB::ShopImage->reorder_list(@{ $::form->{image_id} || [] });
209
210   $self->render(\'', { type => 'json' });
211 }
212
213 sub action_list_articles {
214   my ($self) = @_;
215
216   my %filter      = ($::form->{filter} ? parse_filter($::form->{filter}) : query => [ 'shop.obsolete' => 0 ]);
217   my $sort_by     = $::form->{sort_by} ? $::form->{sort_by} : 'part.partnumber';
218   $sort_by .=$::form->{sort_dir} ? ' DESC' : ' ASC';
219
220   my $articles = SL::DB::Manager::ShopPart->get_all( %filter ,with_objects => [ 'part','shop' ], sort_by => $sort_by );
221
222   foreach my $article (@{ $articles}) {
223     my $images = SL::DB::Manager::ShopImage->get_all_count( where => [ 'files.object_id' => $article->part->id, ], with_objects => 'file', sort_by => 'position' );
224     $article->{images} = $images;
225   }
226
227   $self->render('shop_part/_list_articles', title => t8('Webshops articles'), SHOP_PARTS => $articles);
228 }
229
230 sub action_upload_status {
231   my ($self) = @_;
232   my $job     = SL::DB::BackgroundJob->new(id => $::form->{job_id})->load;
233   my $html    = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
234
235   $self->js->html('#status_mass_upload', $html);
236   $self->js->run('kivi.ShopPart.massUploadFinished') if $job->data_as_hash->{status} == SL::BackgroundJob::ShopPartMassUpload->DONE();
237   $self->js->render;
238 }
239
240 sub action_mass_upload {
241   my ($self) = @_;
242
243   my @shop_parts =  @{ $::form->{shop_parts_ids} || [] };
244
245   my $job = SL::DB::BackgroundJob->new(
246         type                 => 'once',
247         active               => 1,
248         package_name         => 'ShopPartMassUpload',
249         )->set_data(
250         shop_part_record_ids => [ @shop_parts ],
251         todo                 => $::form->{upload_todo},
252         status               => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
253         conversation         => [ ],
254         num_uploaded         => 0,
255    )->update_next_run_at;
256
257    SL::System::TaskServer->new->wake_up;
258
259    my $html = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
260
261    $self->js
262       ->html('#status_mass_upload', $html)
263       ->run('kivi.ShopPart.massUploadStarted')
264       ->render;
265 }
266
267 sub action_update {
268   my ($self) = @_;
269   $self->create_or_update;
270 }
271
272 sub render_shop_part_edit_dialog {
273   my ($self) = @_;
274
275   $self->js
276     ->run(
277       'kivi.ShopPart.shop_part_dialog',
278       t8('Shop part'),
279       $self->render('shop_part/edit', { output => 0 })
280     )
281     ->reinit_widgets;
282
283   $self->js->render;
284 }
285
286 sub create_or_update {
287   my ($self) = @_;
288
289   my $is_new = !$self->shop_part->id;
290   my $params = delete($::form->{shop_part}) || { };
291   $self->shop_part->assign_attributes(%{ $params });
292   $self->shop_part->save;
293
294   my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
295
296   if(!$is_new){
297     flash('info', $is_new ? t8('The shop part has been created.') : t8('The shop part has been saved.'));
298     $self->js->html('#shop_part_description_' . $self->shop_part->id, $self->shop_part->shop_description)
299            ->html('#shop_part_active_' . $self->shop_part->id, $self->shop_part->active)
300            ->html('#price_' . $self->shop_part->id, $::form->format_amount(\%::myconfig,$price,2))
301            ->html('#active_price_source_' . $self->shop_part->id, $price_src_str)
302            ->run('kivi.ShopPart.close_dialog')
303            ->flash('info', t8("Updated shop part"))
304            ->render;
305          }else{
306     $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->shop_part->part_id);
307   }
308 }
309
310 #
311 # internal stuff
312 #
313 sub add_javascripts  {
314   $::request->{layout}->add_javascripts(qw(kivi.ShopPart.js));
315 }
316
317 sub load_pricesources {
318   my ($self) = @_;
319
320   my $pricesources;
321   push( @{ $pricesources } , { id => "master_data/sellprice", name => t8("Master Data")." - ".t8("Sellprice") },
322                              { id => "master_data/listprice", name => t8("Master Data")." - ".t8("Listprice") },
323                              { id => "master_data/lastcost",  name => t8("Master Data")." - ".t8("Lastcost") }
324                              );
325   my $pricegroups = SL::DB::Manager::Pricegroup->get_all;
326   foreach my $pg ( @$pricegroups ) {
327     push( @{ $pricesources } , { id => "pricegroup/".$pg->id, name => t8("Pricegroup") . " - " . $pg->pricegroup} );
328   };
329
330   $self->price_sources( $pricesources );
331 }
332
333 sub get_price_n_pricesource {
334   my ($self,$pricesource) = @_;
335
336   my ( $price_src_str, $price_src_id ) = split(/\//,$pricesource);
337
338   require SL::DB::Pricegroup;
339   require SL::DB::Part;
340   my $price;
341   if ($price_src_str eq "master_data") {
342     my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id );
343     $price         = $part->$price_src_id;
344     $price_src_str = $price_src_id;
345     }else{
346     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];
347     #my $part       = SL::DB::Manager::Part->find_by( id => $self->shop_part->part_id, 'prices.'.pricegroup_id => $price_src_id );
348     my $pricegrp   = SL::DB::Manager::Pricegroup->find_by( id => $price_src_id )->pricegroup;
349     $price         = $part->prices->[0]->price;
350     $price_src_str = $pricegrp;
351   }
352   return($price,$price_src_str);
353 }
354
355 sub check_auth {
356   $::auth->assert('shop_part_edit');
357 }
358
359 sub init_shop_part {
360   if ($::form->{shop_part_id}) {
361     SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
362   } else {
363     SL::DB::ShopPart->new(shop_id => $::form->{shop_id}, part_id => $::form->{part_id});
364   };
365 }
366
367 sub init_file {
368   my $file = $::form->{id} ? SL::DB::File->new(id => $::form->{id})->load : SL::DB::File->new;
369   return $file;
370 }
371
372 sub init_shops {
373   SL::DB::Shop->shops_dd;
374 }
375
376 1;
377
378 __END__
379
380 =encoding utf-8
381
382
383 =head1 NAME
384
385 SL::Controller::ShopPart - Controller for managing ShopParts
386
387 =head1 SYNOPSIS
388
389 ShopParts are configured in a tab of the corresponding part.
390
391 =head1 ACTIONS
392
393 =over 4
394
395 =item C<action_update_shop>
396
397 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.
398
399 =item C<action_show_files>
400
401
402
403 =item C<action_ajax_delete_file>
404
405
406
407 =item C<action_get_categories>
408
409
410
411 =item C<action_show_price_n_pricesource>
412
413
414
415 =item C<action_show_stock>
416
417
418
419 =item C<action_get_n_write_categories>
420
421 Can be used to sync the categories of a shoppart with the categories from online.
422
423 =item C<action_save_categories>
424
425 The ShopwareConnector works with the CategoryID @categories[x][0] in others/new Connectors it must be tested
426 Each assigned categorie is saved with id,categorie_name an multidimensional array and could be expanded with categoriepath or what is needed
427
428 =item C<action_reorder>
429
430
431
432 =item C<action_upload_status>
433
434
435
436 =item C<action_mass_upload>
437
438
439
440 =item C<action_update>
441
442
443
444 =item C<create_or_update>
445
446
447
448 =item C<render_shop_part_edit_dialog>
449
450 when self->shop_part is called in template, it will be an existing shop_part with id,
451 or a new shop_part with only part_id and shop_id set
452
453 =item C<add_javascripts>
454
455
456 =item C<load_pricesources>
457
458 the price sources to use for the article: sellprice, lastcost,
459 listprice, or one of the pricegroups. It overwrites the default pricesource from the shopconfig.
460 TODO: implement valid pricerules for the article
461
462 =item C<get_price_n_pricesource>
463
464
465 =item C<check_auth>
466
467
468 =item C<init_shop_part>
469
470
471 =item C<init_file>
472
473
474 =item C<init_shops>
475
476 data for drop down filter options
477
478 =back
479
480 =head1 TODO
481
482 CheckAuth
483 Pricesrules, pricessources aren't fully implemented yet.
484
485 =head1 AUTHORS
486
487 G. Richardson E<lt>information@kivitendo-premium.deE<gt>
488 W. Hahn E<lt>wh@futureworldsearch.netE<gt>
489
490 =cut