X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FController%2FPart.pm;h=aae4e531e016e0d8da56f6bd0ff5447c1bb7fad5;hb=2b2a37fd95a5cae44a8c6862e79f86d2cbc2a189;hp=27a23daa80a912ac6040ce236199744aca2b695b;hpb=39fa895d374f260293a35734b93ede881e4fed9e;p=kivitendo-erp.git diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index 27a23daa8..aae4e531e 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -6,11 +6,13 @@ use parent qw(SL::Controller::Base); use Clone qw(clone); use SL::DB::Part; use SL::DB::PartsGroup; +use SL::DB::PriceRuleItem; use SL::DB::Shop; use SL::Controller::Helper::GetModels; use SL::Locale::String qw(t8); use SL::JSON; use List::Util qw(sum); +use List::UtilsBy qw(extract_by); use SL::Helper::Flash; use Data::Dumper; use DateTime; @@ -32,13 +34,16 @@ use Rose::Object::MakeMethods::Generic ( all_buchungsgruppen all_payment_terms all_warehouses parts_classification_filter all_languages all_units all_price_factors) ], - 'scalar' => [ qw(warehouse bin) ], + 'scalar' => [ qw(warehouse bin stock_amounts journal) ], ); # safety __PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') }, except => [ qw(ajax_autocomplete part_picker_search part_picker_result) ]); +__PACKAGE__->run_before(sub { $::auth->assert('developer') }, + only => [ qw(test_page) ]); + __PACKAGE__->run_before('check_part_id', only => [ qw(edit delete) ]); # actions for editing parts @@ -123,11 +128,6 @@ sub action_save { # $self->part has been loaded, parsed and validated without errors and is ready to be saved $self->part->db->with_transaction(sub { - if ( $params{save_as_new} ) { - $self->part( $self->part->clone_and_reset_deep ); - $self->part->partnumber(undef); # will be assigned by _before_save_set_partnumber - }; - $self->part->save(cascade => 1); SL::DB::History->new( @@ -139,17 +139,16 @@ sub action_save { )->save(); CVar->save_custom_variables( - dbh => $self->part->db->dbh, - module => 'IC', - trans_id => $self->part->id, - variables => $::form, # $::form->{cvar} would be nicer - always_valid => 1, + dbh => $self->part->db->dbh, + module => 'IC', + trans_id => $self->part->id, + variables => $::form, # $::form->{cvar} would be nicer + save_validity => 1, ); 1; }) or return $self->js->error(t8('The item couldn\'t be saved!') . " " . $self->part->db->error )->render; - ; flash_later('info', $is_new ? t8('The item has been created.') . " " . $self->part->displayable_name : t8('The item has been saved.')); if ( $::form->{callback} ) { @@ -161,9 +160,32 @@ sub action_save { } } -sub action_save_as_new { +sub action_save_and_purchase_order { + my ($self) = @_; + + my $session_value; + if (1 == scalar @{$self->part->makemodels}) { + my $prepared_form = Form->new(''); + $prepared_form->{vendor_id} = $self->part->makemodels->[0]->make; + $session_value = $::auth->save_form_in_session(form => $prepared_form); + } + + $::form->{callback} = $self->url_for( + controller => 'Order', + action => 'return_from_create_part', + type => 'purchase_order', + previousform => $session_value, + ); + + $self->_run_action('save'); +} + +sub action_abort { my ($self) = @_; - $self->action_save(save_as_new=>1); + + if ( $::form->{callback} ) { + $self->redirect_to($::form->unescape($::form->{callback})); + } } sub action_delete { @@ -228,10 +250,12 @@ sub render_form { %assembly_vars = %{ $self->prepare_assembly_render_vars } if $self->part->is_assembly; $params{CUSTOM_VARIABLES} = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id); - $_->{valid} = 1 for @{ $params{CUSTOM_VARIABLES} }; - CVar->render_inputs('variables' => $params{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $self->part->partsgroup_id) - if (scalar @{ $params{CUSTOM_VARIABLES} }); + if (scalar @{ $params{CUSTOM_VARIABLES} }) { + CVar->render_inputs('variables' => $params{CUSTOM_VARIABLES}, show_disabled_message => 1, partsgroup_id => $self->part->partsgroup_id); + $params{CUSTOM_VARIABLES_FIRST_TAB} = []; + @{ $params{CUSTOM_VARIABLES_FIRST_TAB} } = extract_by { $_->{first_tab} == 1 } @{ $params{CUSTOM_VARIABLES} }; + } my %title_hash = ( part => t8('Edit Part'), assembly => t8('Edit Assembly'), @@ -263,14 +287,26 @@ sub action_history { history_entries => $history_entries); } +sub action_inventory { + my ($self) = @_; + + $::auth->assert('warehouse_contents'); + + $self->stock_amounts($self->part->get_simple_stock_sql); + $self->journal($self->part->get_mini_journal); + + $_[0]->render('part/_inventory_data', { layout => 0 }); +}; + sub action_update_item_totals { my ($self) = @_; my $part_type = $::form->{part_type}; die unless $part_type =~ /^(assortment|assembly)$/; - my $sellprice_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'sellcost'); - my $lastcost_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'lastcost'); + my $sellprice_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'sellcost'); + my $lastcost_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'lastcost'); + my $items_weight_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'weight'); my $sum_diff = $sellprice_sum-$lastcost_sum; @@ -280,6 +316,7 @@ sub action_update_item_totals { ->html('#items_sum_diff', $::form->format_amount(\%::myconfig, $sum_diff, 2, 0)) ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0)) ->html('#items_lastcost_sum_basic', $::form->format_amount(\%::myconfig, $lastcost_sum, 2, 0)) + ->html('#items_weight_sum_basic' , $::form->format_amount(\%::myconfig, $items_weight_sum)) ->no_flash_clear->render(); } @@ -385,6 +422,7 @@ sub action_add_assembly_item { my $items_sellprice_sum = $part->items_sellprice_sum; my $items_lastcost_sum = $part->items_lastcost_sum; my $items_sum_diff = $items_sellprice_sum - $items_lastcost_sum; + my $items_weight_sum = $part->items_weight_sum; $self->js ->append('#assembly_rows', $html) # append in tbody @@ -395,27 +433,33 @@ sub action_add_assembly_item { ->html('#items_sum_diff', $::form->format_amount(\%::myconfig, $items_sum_diff , 2, 0)) ->html('#items_sellprice_sum_basic', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0)) ->html('#items_lastcost_sum_basic' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0)) + ->html('#items_weight_sum_basic' , $::form->format_amount(\%::myconfig, $items_weight_sum)) ->render; } sub action_show_multi_items_dialog { + my ($self) = @_; + + my $search_term = $self->models->filtered->laundered->{all_substr_multi__ilike}; + $search_term ||= $self->models->filtered->laundered->{all_with_makemodel_substr_multi__ilike}; + $search_term ||= $self->models->filtered->laundered->{all_with_customer_partnumber_substr_multi__ilike}; + $_[0]->render('part/_multi_items_dialog', { layout => 0 }, - all_partsgroups => SL::DB::Manager::PartsGroup->get_all + all_partsgroups => SL::DB::Manager::PartsGroup->get_all, + search_term => $search_term ); } sub action_multi_items_update_result { - my $max_count = 100; - - $::form->{multi_items}->{filter}->{obsolete} = 0; + my $max_count = $::form->{limit}; my $count = $_[0]->multi_items_models->count; if ($count == 0) { my $text = escape($::locale->text('No results.')); $_[0]->render($text, { layout => 0 }); - } elsif ($count > $max_count) { - my $text = escpae($::locale->text('Too many results (#1 from #2).', $count, $max_count)); + } elsif ($max_count && $count > $max_count) { + my $text = escape($::locale->text('Too many results (#1 from #2).', $count, $max_count)); $_[0]->render($text, { layout => 0 }); } else { my $multi_items = $_[0]->multi_items_models->get; @@ -540,7 +584,7 @@ sub action_warehouse_changed { die unless ref($self->warehouse) eq 'SL::DB::Warehouse'; if ( $self->warehouse->id and @{$self->warehouse->bins} ) { - $self->bin($self->warehouse->bins->[0]); + $self->bin($self->warehouse->bins_sorted->[0]); $self->js ->html('#bin', $self->build_bin_select) ->focus('#part_bin_id'); @@ -564,7 +608,9 @@ sub action_ajax_autocomplete { # since we need a second get models instance with different filters for that, # we only modify the original filter temporarily in place if ($::form->{prefer_exact}) { - local $::form->{filter}{'all::ilike'} = delete local $::form->{filter}{'all:substr:multi::ilike'}; + local $::form->{filter}{'all::ilike'} = delete local $::form->{filter}{'all:substr:multi::ilike'}; + local $::form->{filter}{'all_with_makemodel::ilike'} = delete local $::form->{filter}{'all_with_makemodel:substr:multi::ilike'}; + local $::form->{filter}{'all_with_customer_partnumber::ilike'} = delete local $::form->{filter}{'all_with_customer_partnumber:substr:multi::ilike'}; my $exact_models = SL::Controller::Helper::GetModels->new( controller => $self, @@ -600,7 +646,13 @@ sub action_test_page { } sub action_part_picker_search { - $_[0]->render('part/part_picker_search', { layout => 0 }); + my ($self) = @_; + + my $search_term = $self->models->filtered->laundered->{all_substr_multi__ilike}; + $search_term ||= $self->models->filtered->laundered->{all_with_makemodel_substr_multi__ilike}; + $search_term ||= $self->models->filtered->laundered->{all_with_customer_partnumber_substr_multi__ilike}; + + $_[0]->render('part/part_picker_search', { layout => 0 }, search_term => $search_term); } sub action_part_picker_result { @@ -643,6 +695,8 @@ sub prepare_assortment_render_vars { sub prepare_assembly_render_vars { my ($self) = @_; + croak("Need assembly item(s) to create a 'save as new' assembly.") unless $self->part->items; + my %vars = ( items_sellprice_sum => $self->part->items_sellprice_sum, items_lastcost_sum => $self->part->items_lastcost_sum, assembly_html => $self->render_assembly_items_to_html( \@{ $self->part->items } ), @@ -675,7 +729,7 @@ sub add { sub _set_javascript { my ($self) = @_; - $::request->layout->use_javascript("${_}.js") for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart); + $::request->layout->use_javascript("${_}.js") for qw(kivi.Part kivi.File kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery kivi.ShopPart kivi.Validator); $::request->layout->add_javascripts_inline("\$(function(){kivi.PriceRule.load_price_rules_for_part(@{[ $self->part->id ]})});") if $self->part->id; } @@ -704,7 +758,9 @@ sub recalc_item_totals { } } elsif ( $part->is_assembly ) { $part->assemblies( @{$self->assembly_items} ); - if ( $params{price_type} eq 'lastcost' ) { + if ( $params{price_type} eq 'weight' ) { + return $part->items_weight_sum; + } elsif ( $params{price_type} eq 'lastcost' ) { return $part->items_lastcost_sum; } else { return $part->items_sellprice_sum; @@ -730,6 +786,8 @@ sub parse_form { $self->part->assign_attributes(%{ $params}); $self->part->bin_id(undef) unless $self->part->warehouse_id; + $self->normalize_text_blocks; + # Only reset items ([]) and rewrite from form if $::form->{assortment_items} isn't empty. This # will be the case for used assortments when saving, or when a used assortment # is "used as new" @@ -861,7 +919,7 @@ sub parse_form_customerprices { } sub build_bin_select { - select_tag('part.bin_id', [ $_[0]->warehouse->bins ], + select_tag('part.bin_id', [ @{ $_[0]->warehouse->bins_sorted } ], title_key => 'description', default => $_[0]->bin->id, ); @@ -886,6 +944,8 @@ sub init_part { if ( $::form->{part}{id} ) { return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels customerprices prices translations partsgroup shop_parts shop_parts.shop) ]); + } elsif ( $::form->{id} ) { + return SL::DB::Part->new(id => $::form->{id})->load; # used by inventory tab } else { die "part_type missing" unless $::form->{part}{part_type}; return SL::DB::Part->new(part_type => $::form->{part}{part_type}); @@ -1060,7 +1120,7 @@ sub init_all_price_factors { } sub init_all_pricegroups { - SL::DB::Manager::Pricegroup->get_all_sorted; + SL::DB::Manager::Pricegroup->get_all_sorted(query => [ obsolete => 0 ]); } # model used to filter/display the parts in the multi-items dialog @@ -1192,6 +1252,25 @@ sub check_has_valid_part_type { die "invalid part_type" unless $_[0] =~ /^(part|service|assembly|assortment)$/; } + +sub normalize_text_blocks { + my ($self) = @_; + + # check if feature is enabled (select normalize_part_descriptions from defaults) + return unless ($::instance_conf->get_normalize_part_descriptions); + + # text block + foreach (qw(description)) { + $self->part->{$_} =~ s/\s+$//s; + $self->part->{$_} =~ s/^\s+//s; + $self->part->{$_} =~ s/ {2,}/ /g; + } + # html block (caveat: can be circumvented by using bold or italics) + $self->part->{notes} =~ s/^

( )+\s+/

/s; + $self->part->{notes} =~ s/( )+<\/p>$/<\/p>/s; + +} + sub render_assortment_items_to_html { my ($self, $assortment_items, $number_of_items) = @_; @@ -1266,7 +1345,8 @@ sub parse_add_items_to_objects { sub _setup_form_action_bar { my ($self) = @_; - my $may_edit = $::auth->assert('part_service_assembly_edit', 'may fail'); + my $may_edit = $::auth->assert('part_service_assembly_edit', 'may fail'); + my $used_in_pricerules = !!SL::DB::Manager::PriceRuleItem->get_all_count(where => [type => 'part', value_int => $self->part->id]); for my $bar ($::request->layout->get('actionbar')) { $bar->add( @@ -1275,6 +1355,7 @@ sub _setup_form_action_bar { t8('Save'), call => [ 'kivi.Part.save' ], disabled => !$may_edit ? t8('You do not have the permissions to access this function.') : undef, + checks => ['kivi.validate_form'], ], action => [ t8('Use as new'), @@ -1285,6 +1366,26 @@ sub _setup_form_action_bar { ], ], # end of combobox "Save" + combobox => [ + action => [ t8('Workflow') ], + action => [ + t8('Save and Purchase Order'), + submit => [ '#ic', { action => "Part/save_and_purchase_order" } ], + checks => ['kivi.validate_form'], + disabled => !$self->part->id ? t8('The object has not been saved yet.') + : !$may_edit ? t8('You do not have the permissions to access this function.') + : !$::auth->assert('purchase_order_edit', 'may fail') ? t8('You do not have the permissions to access this function.') + : undef, + only_if => !$::form->{inline_create}, + ], + ], + + action => [ + t8('Abort'), + submit => [ '#ic', { action => "Part/abort" } ], + only_if => !!$::form->{inline_create}, + ], + action => [ t8('Delete'), call => [ 'kivi.Part.delete' ], @@ -1292,6 +1393,7 @@ sub _setup_form_action_bar { disabled => !$self->part->id ? t8('This object has not been saved yet.') : !$may_edit ? t8('You do not have the permissions to access this function.') : !$self->part->orphaned ? t8('This object has already been used.') + : $used_in_pricerules ? t8('This object is used in price rules.') : undef, ],