X-Git-Url: http://wagnertech.de/git?a=blobdiff_plain;f=SL%2FController%2FPart.pm;h=aae4e531e016e0d8da56f6bd0ff5447c1bb7fad5;hb=2b2a37fd95a5cae44a8c6862e79f86d2cbc2a189;hp=350edec138b5725eba052f6bda0cce90ee1a6eda;hpb=5d711a25d9257690164f396b25f57095776790d6;p=kivitendo-erp.git diff --git a/SL/Controller/Part.pm b/SL/Controller/Part.pm index 350edec13..aae4e531e 100644 --- a/SL/Controller/Part.pm +++ b/SL/Controller/Part.pm @@ -5,33 +5,45 @@ 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; use SL::DB::History; use SL::DB::Helper::ValidateAssembly qw(validate_assembly); use SL::CVar; +use SL::MoreCommon qw(save_form); use Carp; +use SL::Presenter::EscapedText qw(escape is_escaped); +use SL::Presenter::Tag qw(select_tag); use Rose::Object::MakeMethods::Generic ( 'scalar --get_set_init' => [ qw(parts models part p warehouses multi_items_models - makemodels + makemodels shops_not_assigned + customerprices orphaned assortment assortment_items assembly assembly_items all_pricegroups all_translations all_partsgroups all_units 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 @@ -64,6 +76,18 @@ sub action_add_assortment { $self->add; }; +sub action_add_from_record { + my ($self) = @_; + + check_has_valid_part_type($::form->{part}{part_type}); + + die 'parts_classification_type must be "sales" or "purchases"' + unless $::form->{parts_classification_type} =~ m/^(sales|purchases)$/; + + $self->parse_form; + $self->add; +} + sub action_add { my ($self) = @_; @@ -89,8 +113,11 @@ sub action_save { return $self->js->error(t8('The document has been changed by another user. Please reopen it in another window and copy the changes to the new window'))->render; } - if ( $is_new and !$::form->{part}{partnumber} ) { - $self->check_next_transnumber_is_free or return $self->js->error(t8('The next partnumber in the number range already exists!'))->render; + if ( $is_new + && $::form->{part}{partnumber} + && SL::DB::Manager::Part->find_by(partnumber => $::form->{part}{partnumber}) + ) { + return $self->js->error(t8('The partnumber is already being used'))->render; } $self->parse_form; @@ -101,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( @@ -117,25 +139,53 @@ 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.') : t8('The item has been saved.')); + flash_later('info', $is_new ? t8('The item has been created.') . " " . $self->part->displayable_name : t8('The item has been saved.')); + + if ( $::form->{callback} ) { + $self->redirect_to($::form->unescape($::form->{callback}) . '&new_parts_id=' . $self->part->id); - # reload item, this also resets last_modification! - $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id); + } else { + # default behaviour after save: reload item, this also resets last_modification! + $self->redirect_to(controller => 'Part', action => 'edit', 'part.id' => $self->part->id); + } } -sub action_save_as_new { +sub action_save_and_purchase_order { my ($self) = @_; - $self->action_save(save_as_new=>1); + + 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) = @_; + + if ( $::form->{callback} ) { + $self->redirect_to($::form->unescape($::form->{callback})); + } } sub action_delete { @@ -163,11 +213,11 @@ sub action_delete { }) or return $self->js->error(t8('The item couldn\'t be deleted!') . " " . $self->part->db->error)->render; flash_later('info', t8('The item has been deleted.')); - my @redirect_params = ( - controller => 'controller.pl', - action => 'LoginScreen/user_login' - ); - $self->redirect_to(@redirect_params); + if ( $::form->{callback} ) { + $self->redirect_to($::form->unescape($::form->{callback})); + } else { + $self->redirect_to(controller => 'ic.pl', action => 'search', searchitems => 'article'); + } } sub action_use_as_new { @@ -193,6 +243,7 @@ sub render_form { my ($self, %params) = @_; $self->_set_javascript; + $self->_setup_form_action_bar; my (%assortment_vars, %assembly_vars); %assortment_vars = %{ $self->prepare_assortment_render_vars } if $self->part->is_assortment; @@ -200,8 +251,11 @@ sub render_form { $params{CUSTOM_VARIABLES} = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id); - 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'), @@ -215,7 +269,6 @@ sub render_form { $self->render( 'part/form', title => $title_hash{$self->part->part_type}, - show_edit_buttons => $::auth->assert('part_service_assembly_edit'), %assortment_vars, %assembly_vars, translations_map => { map { ($_->language_id => $_) } @{$self->part->translations} }, @@ -234,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; @@ -251,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(); } @@ -260,7 +326,7 @@ sub action_add_multi_assortment_items { my $item_objects = $self->parse_add_items_to_objects(part_type => 'assortment'); my $html = $self->render_assortment_items_to_html($item_objects); - $self->js->run('kivi.Part.close_multi_items_dialog') + $self->js->run('kivi.Part.close_picker_dialogs') ->append('#assortment_rows', $html) ->run('kivi.Part.renumber_positions') ->run('kivi.Part.assortment_recalc') @@ -280,7 +346,7 @@ sub action_add_multi_assembly_items { my $html = $self->render_assembly_items_to_html(\@checked_objects); - $self->js->run('kivi.Part.close_multi_items_dialog') + $self->js->run('kivi.Part.close_picker_dialogs') ->append('#assembly_rows', $html) ->run('kivi.Part.renumber_positions') ->run('kivi.Part.assembly_recalc') @@ -356,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 @@ -366,29 +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 { - require SL::DB::PartsGroup; + 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 }, - part_type => 'assortment', - partfilter => '', # can I get at the current input of the partpicker here? - 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 = SL::Presenter::EscapedText->new(text => $::locale->text('No results.')); + my $text = escape($::locale->text('No results.')); $_[0]->render($text, { layout => 0 }); - } elsif ($count > $max_count) { - my $text = SL::Presenter::EscapedText->new(text => $::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; @@ -431,6 +502,39 @@ sub action_add_makemodel_row { ->render; } +sub action_add_customerprice_row { + my ($self) = @_; + + my $customer_id = $::form->{add_customerprice}; + + my $customer = SL::DB::Manager::Customer->find_by(id => $customer_id) + or return $self->js->error(t8("No customer selected or found!"))->render; + + if (grep { $customer_id == $_->customer_id } @{ $self->customerprices }) { + $self->js->flash('info', t8("This customer has already been added.")); + } + + my $position = scalar @{ $self->customerprices } + 1; + + my $cu = SL::DB::PartCustomerPrice->new( + customer_id => $customer->id, + customer_partnumber => '', + price => 0, + sortorder => $position, + ) or die "Can't create Customerprice object"; + + my $row_as_html = $self->p->render( + 'part/_customerprice_row', + customerprice => $cu, + listrow => $position % 2 ? 0 + : 1, + ); + + $self->js->append('#customerprice_rows', $row_as_html) # append in tbody + ->val('.add_customerprice_input', '') + ->run('kivi.Part.focus_last_customerprice_input')->render; +} + sub action_reorder_items { my ($self) = @_; @@ -480,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'); @@ -501,22 +605,21 @@ sub action_ajax_autocomplete { # if someone types something, and hits enter, assume he entered the full name. # if something matches, treat that as sole match - # unfortunately get_models can't do more than one per package atm, so we d it - # the oldfashioned way. + # 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_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, + sorted => 0, + paginated => { per_page => 2 }, + with_objects => [ qw(unit_obj classification) ], + ); my $exact_matches; - if (1 == scalar @{ $exact_matches = SL::DB::Manager::Part->get_all( - query => [ - obsolete => 0, - SL::DB::Manager::Part->type_filter($::form->{filter}{part_type}), - SL::DB::Manager::PartClassification->classification_filter($::form->{filter}{classification_id}), - or => [ - description => { ilike => $::form->{filter}{'all:substr:multi::ilike'} }, - partnumber => { ilike => $::form->{filter}{'all:substr:multi::ilike'} }, - ] - ], - limit => 2, - ) }) { + if (1 == scalar @{ $exact_matches = $exact_models->get }) { $self->parts($exact_matches); } } @@ -528,6 +631,7 @@ sub action_ajax_autocomplete { id => $_->id, partnumber => $_->partnumber, description => $_->description, + ean => $_->ean, part_type => $_->part_type, unit => $_->unit, cvars => { map { ($_->config->name => { value => $_->value_as_text, is_valid => $_->is_valid }) } @{ $_->cvars_by_config } }, @@ -542,11 +646,17 @@ sub action_test_page { } sub action_part_picker_search { - $_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts); + 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 { - $_[0]->render('part/_part_picker_result', { layout => 0 }); + $_[0]->render('part/_part_picker_result', { layout => 0 }, parts => $_[0]->parts); } sub action_show { @@ -585,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 } ), @@ -600,6 +712,7 @@ sub add { check_has_valid_part_type($self->part->part_type); $self->_set_javascript; + $self->_setup_form_action_bar; my %title_hash = ( part => t8('Add Part'), assembly => t8('Add Assembly'), @@ -609,15 +722,14 @@ sub add { $self->render( 'part/form', - title => $title_hash{$self->part->part_type}, - show_edit_buttons => $::auth->assert('part_service_assembly_edit'), + title => $title_hash{$self->part->part_type}, ); } sub _set_javascript { my ($self) = @_; - $::request->layout->use_javascript("${_}.js") for qw(kivi.Part kivi.PriceRule ckeditor/ckeditor ckeditor/adapters/jquery); + $::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; } @@ -646,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; @@ -669,11 +783,11 @@ sub parse_form { my $params = delete($::form->{part}) || { }; delete $params->{id}; - # never overwrite existing partnumber, should be a read-only field anyway - delete $params->{partnumber} if $self->part->partnumber; $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" @@ -693,6 +807,7 @@ sub parse_form { $self->part->prices([]); $self->parse_form_prices; + $self->parse_form_customerprices; $self->parse_form_makemodels; } @@ -739,8 +854,9 @@ sub parse_form_makemodels { $position++; my $vendor = SL::DB::Manager::Vendor->find_by(id => $makemodel->{make}) || die "Can't find vendor from make"; + my $id = $makemodels_map->{$makemodel->{id}} ? $makemodels_map->{$makemodel->{id}}->id : undef; my $mm = SL::DB::MakeModel->new( # parts_id => $self->part->id, # will be assigned by row add_makemodels - id => $makemodel->{id}, + id => $id, make => $makemodel->{make}, model => $makemodel->{model} || '', lastcost => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number}), @@ -762,13 +878,54 @@ sub parse_form_makemodels { }; } +sub parse_form_customerprices { + my ($self) = @_; + + my $customerprices_map; + if ( $self->part->customerprices ) { # check for new parts or parts without customerprices + $customerprices_map = { map { $_->id => Rose::DB::Object::Helpers::clone($_) } @{$self->part->customerprices} }; + }; + + $self->part->customerprices([]); + + my $position = 0; + my $customerprices = delete($::form->{customerprices}) || []; + foreach my $customerprice ( @{$customerprices} ) { + next unless $customerprice->{customer_id}; + $position++; + my $customer = SL::DB::Manager::Customer->find_by(id => $customerprice->{customer_id}) || die "Can't find customer from id"; + + my $id = $customerprices_map->{$customerprice->{id}} ? $customerprices_map->{$customerprice->{id}}->id : undef; + my $cu = SL::DB::PartCustomerPrice->new( # parts_id => $self->part->id, # will be assigned by row add_customerprices + id => $id, + customer_id => $customerprice->{customer_id}, + customer_partnumber => $customerprice->{customer_partnumber} || '', + price => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number}), + sortorder => $position, + ); + if ($customerprices_map->{$cu->id} && !$customerprices_map->{$cu->id}->lastupdate && $customerprices_map->{$cu->id}->price == 0 && $cu->price == 0) { + # lastupdate isn't set, original price is 0 and new lastcost is 0 + # don't change lastupdate + } elsif ( !$customerprices_map->{$cu->id} && $cu->price == 0 ) { + # new customerprice, no lastcost entered, leave lastupdate empty + } elsif ($customerprices_map->{$cu->id} && $customerprices_map->{$cu->id}->price == $cu->price) { + # price hasn't changed, use original lastupdate + $cu->lastupdate($customerprices_map->{$cu->id}->lastupdate); + } else { + $cu->lastupdate(DateTime->now); + }; + $self->part->add_customerprices($cu); + }; +} + sub build_bin_select { - $_[0]->p->select_tag('part.bin_id', [ $_[0]->warehouse->bins ], + select_tag('part.bin_id', [ @{ $_[0]->warehouse->bins_sorted } ], title_key => 'description', default => $_[0]->bin->id, ); } + # get_set_inits for partpicker sub init_parts { @@ -786,7 +943,9 @@ sub init_part { # used by edit, save, delete and add if ( $::form->{part}{id} ) { - return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels prices translations partsgroup) ]); + 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}); @@ -865,6 +1024,29 @@ sub init_makemodels { return \@makemodel_array; } +sub init_customerprices { + my ($self) = @_; + + my $position = 0; + my @customerprice_array = (); + my $customerprices = delete($::form->{customerprices}) || []; + + foreach my $customerprice ( @{$customerprices} ) { + next unless $customerprice->{customer_id}; + $position++; + my $cu = SL::DB::PartCustomerPrice->new( # parts_id => $self->part->id, # will be assigned by row add_customerprices + id => $customerprice->{id}, + customer_partnumber => $customerprice->{customer_partnumber}, + customer_id => $customerprice->{customer_id} || '', + price => $::form->parse_amount(\%::myconfig, $customerprice->{price_as_number} || 0), + sortorder => $position, + ) or die "Can't create cu"; + # $cu->id($customerprice->{id}) if $customerprice->{id}; + push(@customerprice_array, $cu); + }; + return \@customerprice_array; +} + sub init_assembly_items { my ($self) = @_; my $position = 0; @@ -903,7 +1085,19 @@ sub init_all_buchungsgruppen { if ( $self->part->orphaned ) { return SL::DB::Manager::Buchungsgruppe->get_all_sorted; } else { - return SL::DB::Manager::Buchungsgruppe->get_all(where => [ id => $self->part->buchungsgruppen_id ]); + return SL::DB::Manager::Buchungsgruppe->get_all_sorted(where => [ id => $self->part->buchungsgruppen_id ]); + } +} + +sub init_shops_not_assigned { + my ($self) = @_; + + my @used_shop_ids = map { $_->shop->id } @{ $self->part->shop_parts }; + if ( @used_shop_ids ) { + return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0, '!id' => \@used_shop_ids ], sort_by => 'sortkey' ); + } + else { + return SL::DB::Manager::Shop->get_all( query => [ obsolete => 0 ], sort_by => 'sortkey' ); } } @@ -926,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 @@ -947,6 +1141,15 @@ sub init_multi_items_models { ); } +sub init_parts_classification_filter { + return [] unless $::form->{parts_classification_type}; + + return [ used_for_sale => 't' ] if $::form->{parts_classification_type} eq 'sales'; + return [ used_for_purchase => 't' ] if $::form->{parts_classification_type} eq 'purchases'; + + die "no query rules for parts_classification_type " . $::form->{parts_classification_type}; +} + # simple checks to run on $::form before saving sub form_check_part_description_exists { @@ -964,7 +1167,7 @@ sub form_check_assortment_items_exist { my ($self) = @_; return 1 unless $::form->{part}{part_type} eq 'assortment'; - # skip check for existing parts that have been used + # skip item check for existing assortments that have been used return 1 if ($self->part->id and !$self->part->orphaned); # new or orphaned parts must have items in $::form->{assortment_items} @@ -1001,6 +1204,9 @@ sub form_check_assembly_items_exist { return 1 unless $::form->{part}->{part_type} eq 'assembly'; + # skip item check for existing assembly that have been used + return 1 if ($self->part->id and !$self->part->orphaned); + unless ( $::form->{assembly_items} and scalar @{$::form->{assembly_items}} ) { $self->js->run('kivi.Part.set_tab_active_by_name', 'assembly_tab') ->focus('#add_assembly_item_name') @@ -1025,17 +1231,6 @@ sub form_check_partnumber_is_unique { } # general checking functions -sub check_next_transnumber_is_free { - my ($self) = @_; - - my ($next_transnumber, $count); - $self->part->db->with_transaction(sub { - $next_transnumber = $self->part->get_next_trans_number; - $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $next_transnumber ]); - return 1; - }) or die $@; - $count ? return 0 : return 1; -} sub check_part_id { die t8("Can't load item without a valid part.id") . "\n" unless $::form->{part}{id}; @@ -1057,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) = @_; @@ -1128,6 +1342,74 @@ sub parse_add_items_to_objects { return \@item_objects; } +sub _setup_form_action_bar { + my ($self) = @_; + + 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( + combobox => [ + action => [ + 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'), + call => [ 'kivi.Part.use_as_new' ], + 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.') + : undef, + ], + ], # 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' ], + confirm => t8('Do you really want to delete this object?'), + 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, + ], + + 'separator', + + action => [ + t8('History'), + call => [ 'kivi.Part.open_history_popup' ], + 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.') + : undef, + ], + ); + } +} + 1; __END__ @@ -1227,6 +1509,17 @@ parameter part_type as an action. Example: controller.pl?action=Part/add&part_type=service +=item C + +When adding new items to records they can be created on the fly if the entered +partnumber or description doesn't exist yet. After being asked what part type +the new item should have the user is redirected to the correct edit page. + +Depending on whether the item was added from a sales or a purchase record, only +the relevant part classifications should be selectable for new item, so this +parameter is passed on via a hidden parts_classification_type in the new_item +template. + =item C Saves the current part and then reloads the edit page for the part.