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
$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) = @_;
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;
# $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(
)->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 {
}) 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 {
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;
$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'),
$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} },
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;
->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();
}
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')
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')
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
->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;
->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) = @_;
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');
# 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);
}
}
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 } },
}
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 {
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 } ),
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'),
$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.File 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;
}
}
} 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;
my $params = delete($::form->{part}) || { };
delete $params->{id};
- # never overwrite existing partnumber for parts in use, should be a read-only field in that case anyway
- delete $params->{partnumber} if $self->part->partnumber and not $self->orphaned;
$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"
$self->part->prices([]);
$self->parse_form_prices;
+ $self->parse_form_customerprices;
$self->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}),
};
}
+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 {
# 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});
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;
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' );
}
}
}
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
);
}
+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 {
}
# 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};
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/^<p>( )+\s+/<p>/s;
+ $self->part->{notes} =~ s/( )+<\/p>$/<\/p>/s;
+
+}
+
sub render_assortment_items_to_html {
my ($self, $assortment_items, $number_of_items) = @_;
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__
controller.pl?action=Part/add&part_type=service
+=item C<action_add_from_record>
+
+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<action_save>
Saves the current part and then reloads the edit page for the part.