1 package SL::Controller::ShopPart;
5 use parent qw(SL::Controller::Base);
7 use SL::BackgroundJob::ShopPartMassUpload;
8 use SL::System::TaskServer;
10 use SL::Locale::String qw(t8);
14 use SL::DB::ShopImage;
16 use SL::Helper::Flash;
17 use SL::Controller::Helper::ParseFilter;
20 use Rose::Object::MakeMethods::Generic
22 scalar => [ qw(price_sources) ],
23 'scalar --get_set_init' => [ qw(shop_part file shops) ],
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) ]);
34 sub action_create_or_edit_popup {
37 $self->render_shop_part_edit_dialog();
40 sub action_update_shop {
41 my ($self, %params) = @_;
43 my $shop_part = SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
44 die unless $shop_part;
47 my $shop = SL::Shop->new( config => $shop_part->shop );
49 my $connect = $shop->check_connectivity;
50 if($connect->{success}){
51 my $return = $shop->connector->update_part($self->shop_part, 'all');
53 # the connector deals with parsing/result verification, just needs to return success or failure
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') ) )
63 $self->js->flash('error', t8('The shop part wasn\'t updated.'))->render;
66 $self->js->flash('error', t8('The shop part wasn\'t updated. #1', $connect->{data}->{version}))->render;
72 sub action_show_files {
75 my $images = SL::DB::Manager::ShopImage->get_all( where => [ 'files.object_id' => $::form->{id}, ], with_objects => 'file', sort_by => 'position' );
77 $self->render('shop_part/_list_images', { header => 0 }, IMAGES => $images);
80 sub action_ajax_delete_file {
85 ->run('kivi.ShopPart.show_images',$self->file->object_id)
89 sub action_get_shop_parts {
91 $main::lxdebug->dump(0, "TST: ShopPart get_shop_parts form", $::form);
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};
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 ;
107 flash_later('error', t8('From shop "#1" : Number: #2 #3 ', $shop->config->description, $part_number, t8('Partnumber is already exist')));
110 flash_later('error', t8('Shop or partnumber not selected.'));
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 );
117 $new_parts = $shop->connector->get_shop_parts;
118 push @{ $parts_fetched }, $new_parts ;
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},));
126 flash_later('info', t8('From shop #1 : #2 parts have been imported.', $shop_fetched->{description}, $shop_fetched->{number_of_parts},));
130 $self->redirect_to(controller => "ShopPart", action => 'list_articles');
133 sub action_get_categories {
137 my $shop = SL::Shop->new( config => $self->shop_part->shop );
139 my $connect = $shop->check_connectivity;
140 if($connect->{success}){
141 my $categories = $shop->connector->get_categories;
145 'kivi.ShopPart.shop_part_dialog',
146 t8('Shopcategories'),
147 $self->render('shop_part/categories', { output => 0 }, CATEGORIES => $categories )
152 $self->js->flash('error', t8('Can\'t connect to shop. #1', $connect->{data}->{version}))->render;
157 sub action_show_price_n_pricesource {
160 my ( $price, $price_src_str ) = $self->get_price_n_pricesource($::form->{pricesource});
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');
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)
174 sub action_show_stock {
176 my ( $stock_local, $stock_onlineshop, $active_online );
179 my $shop = SL::Shop->new( config => $self->shop_part->shop );
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};
187 $stock_local = $self->shop_part->part->onhand;
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)
194 sub action_get_n_write_categories {
197 my @shop_parts = @{ $::form->{shop_parts_ids} || [] };
198 foreach my $part(@shop_parts){
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};
206 for(keys %$online_cat){
208 push @cattmp,$online_cat->{$_}->{id};
209 push @cattmp,$online_cat->{$_}->{name};
212 my $attributes->{shop_category} = \@cat;
213 my $active->{active} = $online_article->{data}->{active};
214 $shop_part->assign_attributes(%{$attributes}, %{$active});
217 $self->redirect_to( action => 'list_articles' );
220 sub action_save_categories {
223 my @categories = @{ $::form->{categories} || [] };
226 foreach my $cat ( @categories) {
228 push( @cattmp,$cat );
229 push( @cattmp,$::form->{"cat_id_${cat}"} );
230 push( @cat,\@cattmp );
233 my $categories->{shop_category} = \@cat;
235 my $params = delete($::form->{shop_part}) || { };
237 $self->shop_part->assign_attributes(%{ $params });
238 $self->shop_part->assign_attributes(%{ $categories });
240 $self->shop_part->save;
242 flash('info', t8('The categories has been saved.'));
244 $self->js->run('kivi.ShopPart.close_dialog')
245 ->flash('info', t8("Updated categories"))
251 require SL::DB::ShopImage;
252 SL::DB::ShopImage->reorder_list(@{ $::form->{image_id} || [] });
254 $self->render(\'', { type => 'json' });
257 sub action_list_articles {
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';
264 my $articles = SL::DB::Manager::ShopPart->get_all( %filter ,with_objects => [ 'part','shop' ], sort_by => $sort_by );
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;
271 $self->_setup_list_action_bar;
273 $self->render('shop_part/_list_articles', title => t8('Webshops articles'), SHOP_PARTS => $articles);
276 sub action_upload_status {
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);
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();
286 sub action_mass_upload {
289 my @shop_parts = @{ $::form->{shop_parts_ids} || [] };
291 my $job = SL::DB::BackgroundJob->new(
294 package_name => 'ShopPartMassUpload',
296 shop_part_record_ids => [ @shop_parts ],
297 todo => $::form->{upload_todo},
298 status => SL::BackgroundJob::ShopPartMassUpload->WAITING_FOR_EXECUTION(),
301 )->update_next_run_at;
303 SL::System::TaskServer->new->wake_up;
305 my $html = $self->render('shop_part/_upload_status', { output => 0 }, job => $job);
308 ->html('#status_mass_upload', $html)
309 ->run('kivi.ShopPart.massUploadStarted')
316 $self->create_or_update;
319 sub render_shop_part_edit_dialog {
324 'kivi.ShopPart.shop_part_dialog',
326 $self->render('shop_part/edit', { output => 0 })
333 sub create_or_update {
336 my $is_new = !$self->shop_part->id;
338 my $params = delete($::form->{shop_part}) || { };
340 $self->shop_part->assign_attributes(%{ $params });
342 $self->shop_part->save;
344 my ( $price, $price_src_str ) = $self->get_price_n_pricesource($self->shop_part->active_price_source);
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"))
355 $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->shop_part->part_id);
362 sub add_javascripts {
363 $::request->{layout}->add_javascripts(qw(kivi.ShopPart.js));
366 sub load_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") }
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} );
379 $self->price_sources( $pricesources );
382 sub get_price_n_pricesource {
383 my ($self,$pricesource) = @_;
385 my ( $price_src_str, $price_src_id ) = split(/\//,$pricesource);
387 require SL::DB::Pricegroup;
388 require SL::DB::Part;
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;
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;
401 return($price,$price_src_str);
404 sub _setup_list_action_bar {
407 for my $bar ($::request->layout->get('actionbar')) {
411 submit => [ '#shop_part_filter', { action => "ShopPart/list_articles" } ],
415 call => [ 'kivi.ShopPart.get_shop_parts_one_setup' ],
416 tooltip => t8('Get one part by partnumber'),
420 call => [ 'kivi.ShopPart.get_shop_parts_new' ],
421 tooltip => t8('Get all new parts'),
428 $::auth->assert('shop_part_edit');
432 if ($::form->{shop_part_id}) {
433 SL::DB::Manager::ShopPart->find_by(id => $::form->{shop_part_id});
435 SL::DB::ShopPart->new(shop_id => $::form->{shop_id}, part_id => $::form->{part_id});
440 my $file = $::form->{id} ? SL::DB::File->new(id => $::form->{id})->load : SL::DB::File->new;
445 SL::DB::Shop->shops_dd;
457 SL::Controller::ShopPart - Controller for managing ShopParts
461 ShopParts are configured in a tab of the corresponding part.
467 =item C<action_update_shop>
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.
471 =item C<action_show_files>
475 =item C<action_ajax_delete_file>
479 =item C<action_get_categories>
483 =item C<action_show_price_n_pricesource>
487 =item C<action_show_stock>
491 =item C<action_get_n_write_categories>
493 Can be used to sync the categories of a shoppart with the categories from online.
495 =item C<action_save_categories>
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
500 =item C<action_reorder>
504 =item C<action_upload_status>
508 =item C<action_mass_upload>
512 =item C<action_update>
516 =item C<create_or_update>
520 =item C<render_shop_part_edit_dialog>
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
525 =item C<add_javascripts>
528 =item C<load_pricesources>
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
534 =item C<get_price_n_pricesource>
540 =item C<init_shop_part>
548 data for drop down filter options
555 Pricesrules, pricessources aren't fully implemented yet.
559 G. Richardson E<lt>information@kivitendo-premium.deE<gt>
560 W. Hahn E<lt>wh@futureworldsearch.netE<gt>