Projekt

Allgemein

Profil

Herunterladen (83,7 KB) Statistiken
| Zweig: | Markierung: | Revision:
package SL::Controller::Part;

use strict;
use parent qw(SL::Controller::Base);

use Carp;
use Clone qw(clone);
use Data::Dumper;
use DateTime;
use File::Temp;
use List::Util qw(sum);
use List::UtilsBy qw(extract_by);
use List::MoreUtils qw(any pairwise);
use POSIX qw(strftime);
use Text::CSV_XS;

use SL::CVar;
use SL::Controller::Helper::GetModels;
use SL::DB::Business;
use SL::DB::BusinessModel;
use SL::DB::Helper::ValidateAssembly qw(validate_assembly);
use SL::DB::History;
use SL::DB::Part;
use SL::DB::PartsGroup;
use SL::DB::PriceRuleItem;
use SL::DB::PurchaseBasketItem;
use SL::DB::Shop;
use SL::DB::VariantProperty;
use SL::DB::VariantPropertyPart;
use SL::DB::VariantPropertyValuePart;
use SL::Helper::Flash;
use SL::Helper::PrintOptions;
use SL::JSON;
use SL::Locale::String qw(t8);
use SL::MoreCommon qw(save_form);
use SL::Presenter::EscapedText qw(escape is_escaped);
use SL::Presenter::Part;
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 businessmodels 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
all_businesses) ],
'scalar' => [ qw(warehouse bin stock_amounts journal) ],
);

# safety
__PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit', 1) || $::auth->assert('part_service_assembly_details') },
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
#
sub action_add_part {
my ($self, %params) = @_;

$self->part( SL::DB::Part->new_part );
$self->add;
};

sub action_add_service {
my ($self, %params) = @_;

$self->part( SL::DB::Part->new_service );
$self->add;
};

sub action_add_assembly {
my ($self, %params) = @_;

$self->part( SL::DB::Part->new_assembly );
$self->add;
};

sub action_add_assortment {
my ($self, %params) = @_;

$self->part( SL::DB::Part->new_assortment );
$self->add;
};

sub action_add_parent_variant {
my ($self, %params) = @_;

$self->part( SL::DB::Part->new_parent_variant );
$self->part->part_type($::form->{part_type});
$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) = @_;

check_has_valid_part_type($::form->{part_type});

$self->action_add_part if $::form->{part_type} eq 'part';
$self->action_add_service if $::form->{part_type} eq 'service';
$self->action_add_assembly if $::form->{part_type} eq 'assembly';
$self->action_add_assortment if $::form->{part_type} eq 'assortment';
};

sub action_save {
my ($self, %params) = @_;

# checks that depend only on submitted $::form
$self->check_form or return $self->js->render;

my $is_new = !$self->part->id; # $ part gets loaded here

# check that the part hasn't been modified
unless ( $is_new ) {
$self->check_part_not_modified or
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
&& $::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;

my @errors = $self->part->validate;
return $self->js->error(@errors)->render if @errors;

if ($is_new) {
# Ensure CVars that should be enabled by default actually are when
# creating new parts.
my @default_valid_configs =
grep { ! $_->{flag_defaults_to_invalid} }
grep { $_->{module} eq 'IC' }
@{ CVar->get_configs() };

$::form->{"cvar_" . $_->{name} . "_valid"} = 1 for @default_valid_configs;
} else {
$self->{lastcost_modified} = $self->check_lastcost_modified;
}

# $self->part has been loaded, parsed and validated without errors and is ready to be saved
$self->part->db->with_transaction(sub {

$self->part->save(cascade => 1);
$self->part->set_lastcost_assemblies_and_assortiments if $self->{lastcost_modified};

SL::DB::History->new(
trans_id => $self->part->id,
snumbers => 'partnumber_' . $self->part->partnumber,
employee_id => SL::DB::Manager::Employee->current->id,
what_done => 'part',
addition => 'SAVED',
)->save();

CVar->save_custom_variables(
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} ) {
$self->redirect_to($::form->unescape($::form->{callback}) . '&new_parts_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_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) = @_;

if ( $::form->{callback} ) {
$self->redirect_to($::form->unescape($::form->{callback}));
}
}

sub action_update_variant_property_value_options {
my ($self) = @_;

my %select_tag_options = (
title_key => 'displayable_name',
value_key => 'id',
);

my @options;
my $variant_property_id = $::form->{add_variant_property};
if ($variant_property_id) {
my $variant_property = SL::DB::VariantProperty->new(id => $variant_property_id)->load;
@options = $variant_property->property_values;
}

unless (scalar @options) {
$select_tag_options{with_empty} = 1;
$select_tag_options{empty_title} = t8("Select Variant Property First");
}

my $new_select_tag = select_tag(
"variants[].add_variant_property_value", \@options,
%select_tag_options
);
$self->js->replaceWith('[name^="variants[].add_variant_property_value"]', $new_select_tag);

my $new_select_tag_multible = select_tag(
"add_variant_property_value_for_selected_variants", \@options,
%select_tag_options
);
$self->js->replaceWith("#add_variant_property_value_for_selected_variants", $new_select_tag_multible);

$self->js->render();
}

sub action_update_variants {
my ($self) = @_;

my %variant_id_to_values = map {$_->{id} => $_} @{$::form->{variants}};

my $variant_property_id = $::form->{add_variant_property};
if ($variant_property_id) {
foreach my $variant (@{$self->part->variants}) {
die t8("Please select a new variant property value for all variants")
unless $variant_id_to_values{$variant->id}->{"add_variant_property_value"};
}
}

SL::DB->client->with_transaction(sub {
my $new_variant_property;
if ($variant_property_id) {
$new_variant_property = SL::DB::VariantPropertyPart->new(
part_id => $self->part->id,
variant_property_id => $variant_property_id,
)->save;
}
foreach my $variant (@{$self->part->variants}) {
my $variant_attributes = $variant_id_to_values{$variant->id};
my $variant_property_value_id = delete $variant_attributes->{add_variant_property_value};
delete $variant_attributes->{$_} for qw(id position);
$variant->update_attributes(%$variant_attributes);
if ($new_variant_property) {
SL::DB::VariantPropertyValuePart->new(
part_id => $variant->id,
variant_property_value_id => $variant_property_value_id,
)->save;
}
}
1;
}) or do {
return $self->js->error(t8('Error while adding variant property: #1', SL::DB->client->error))->render();
};

$self->redirect_to(
controller => 'Part',
action => 'edit',
'part.id' => $self->part->id
);
}

sub action_create_variants {
my ($self) = @_;
my @variant_property_ids = sort keys %{$::form->{variant_properties}};

my $part = $self->part;
my $variant_properties = $part->variant_properties();

if (any {$_} grep {
!defined $::form->{variant_properties}->{$_->id}->{selected_property_value_ids}
} @$variant_properties
) {
return $self->js->error(
t8('No property value selected for variant properties: #1.',
join(", ",
map {$_->displayable_name}
grep {!defined $::form->{variant_properties}->{$_->id}->{selected_property_value_ids}}
@$variant_properties
)
)
)->render();
}

my @grouped_variant_property_values = ();
push @grouped_variant_property_values,
SL::DB::Manager::VariantPropertyValue->get_all(
where => [ id => $::form->{variant_properties}->{$_}->{selected_property_value_ids} ]
)
for @variant_property_ids;


my @variant_property_value_lists = ();
foreach my $variant_property_values (@grouped_variant_property_values) {
my @new_lists = ();
foreach my $variant_property_value (@$variant_property_values) {
unless (scalar @variant_property_value_lists) {
push @new_lists, [$variant_property_value];
}
foreach my $variant_property_values_list (@variant_property_value_lists) {
push @new_lists, [@$variant_property_values_list, $variant_property_value];
}
}
@variant_property_value_lists = @new_lists;
}

_check_variant_property_values_not_taken($part, \@variant_property_value_lists);
SL::DB->client->with_transaction(sub {
$part->create_new_variant($_) for @variant_property_value_lists;
1;
}) or do {
die t8('Error while creating variants: '), SL::DB->client->error;
};

$self->redirect_to(
controller => 'Part',
action => 'edit',
'part.id' => $self->part->id
);
}

sub action_convert_part_to_variant {
my ($self) = @_;

my $convert_part_id = $::form->{convert_part}->{id};
die t8("Please select a part to convert.") unless $convert_part_id;

my @variant_property_ids = sort keys %{$::form->{convert_part}->{variant_properties}};

my $part = $self->part;
my $variant_properties = $part->variant_properties();
my @needed_variant_property_ids = sort map {$_->id} @$variant_properties;

if (@variant_property_ids != @needed_variant_property_ids) {
return $self->js->error(
t8('No property value selected for variant properties: #1.',
join(", ",
map {$_->displayable_name}
grep {!defined $::form->{convert_part}->{variant_properties}->{$_->id}->{selected_property_value_id}}
@$variant_properties
)
)
)->render();
}

my @variant_property_values = map {
SL::DB::VariantPropertyValue->new(
id => $::form->{convert_part}->{variant_properties}->{$_}->{selected_property_value_id}
)->load()
} @variant_property_ids;

_check_variant_property_values_not_taken($part, [\@variant_property_values]);
SL::DB->client->with_transaction(sub {
my $part_to_convert = SL::DB::Part->new(id => $convert_part_id)->load;
$part_to_convert->variant_type('variant');
$part_to_convert->variant_property_values(\@variant_property_values);
$part_to_convert->parent_variant($part);
$part_to_convert->save;
1;
}) or do {
die t8('Error while converting part to variant: '), SL::DB->Client->error;
};

$self->redirect_to(
controller => 'Part',
action => 'edit',
'part.id' => $self->part->id
);
}

sub action_delete {
my ($self) = @_;

my $db = $self->part->db; # $self->part has a get_set_init on $::form

my $partnumber = $self->part->partnumber; # remember for history log

$db->do_transaction(
sub {

# delete part, together with relationships that don't already
# have an ON DELETE CASCADE, e.g. makemodel and translation.
$self->part->delete(cascade => 1);

SL::DB::History->new(
trans_id => $self->part->id,
snumbers => 'partnumber_' . $partnumber,
employee_id => SL::DB::Manager::Employee->current->id,
what_done => 'part',
addition => 'DELETED',
)->save();
1;
}) 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.'));
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) = @_;

my $oldpart = SL::DB::Manager::Part->find_by( id => $::form->{old_id}) or die "can't find old part";
$::form->{oldpartnumber} = $oldpart->partnumber;

$self->part($oldpart->clone_and_reset_deep);
$self->parse_form(use_as_new => 1);
$self->part->partnumber(undef);
$self->render_form(use_as_new => 1);
}

sub action_edit {
my ($self, %params) = @_;

$self->render_form;
}

sub action_add_to_basket {
my ( $self ) = @_;

if ( !$self->_is_in_purchase_basket && scalar @{$self->part->makemodels}) {

my $part = $self->part;

my $needed_qty = $part->order_qty < ($part->rop - $part->onhandqty) ?
$part->rop - $part->onhandqty
: $part->order_qty;

my $basket_part = SL::DB::PurchaseBasketItem->new(
part_id => $part->id,
qty => $needed_qty,
orderer => SL::DB::Manager::Employee->current,
)->save;

$self->js->flash('info', t8('Part added to purchasebasket'))->render;
} else {
$self->js->flash('error', t8('Part already in purchasebasket or has no vendor'))->render;
}
return 1;
}

sub render_form {
my ($self, %params) = @_;

$self->_set_javascript;
$self->_setup_form_action_bar;

my (%assortment_vars, %assembly_vars, %parent_variant_vars, %variant_vars);
%assortment_vars = %{ $self->prepare_assortment_render_vars } if $self->part->is_assortment;
%assembly_vars = %{ $self->prepare_assembly_render_vars } if $self->part->is_assembly;
%parent_variant_vars = %{ $self->prepare_parent_variant_render_vars } if $self->part->is_parent_variant;

$params{CUSTOM_VARIABLES} = $params{use_as_new} && $::form->{old_id}
? CVar->get_custom_variables(module => 'IC', trans_id => $::form->{old_id})
: CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id);


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'),
service => t8('Edit Service'),
assortment => t8('Edit Assortment'),
);
my $title = $title_hash{$self->part->part_type};
$title .=
' (' . SL::Presenter::Part::variant_type_abbreviation($self->part->variant_type) . ')'
if $self->part->variant_type;

$self->part->prices([]) unless $self->part->prices;
$self->part->translations([]) unless $self->part->translations;

$self->render(
'part/form',
title => $title,
%assortment_vars,
%assembly_vars,
%parent_variant_vars,
translations_map => { map { ($_->language_id => $_) } @{$self->part->translations} },
prices_map => { map { ($_->pricegroup_id => $_) } @{$self->part->prices } },
oldpartnumber => $::form->{oldpartnumber},
old_id => $::form->{old_id},
%params,
);
}

sub action_history {
my ($self) = @_;

my $history_entries = SL::DB::Part->new(id => $::form->{part}{id})->history_entries;
$_[0]->render('part/history', { layout => 0 },
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 $items_weight_sum = $self->recalc_item_totals(part_type => $part_type, price_type => 'weight');

my $sum_diff = $sellprice_sum-$lastcost_sum;

$self->js
->html('#items_sellprice_sum', $::form->format_amount(\%::myconfig, $sellprice_sum, 2, 0))
->html('#items_lastcost_sum', $::form->format_amount(\%::myconfig, $lastcost_sum, 2, 0))
->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();
}

sub action_add_multi_assortment_items {
my ($self) = @_;

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_picker_dialogs')
->append('#assortment_rows', $html)
->run('kivi.Part.renumber_positions')
->run('kivi.Part.assortment_recalc')
->render();
}

sub action_add_multi_assembly_items {
my ($self) = @_;

my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly');
my @checked_objects;
foreach my $item (@{$item_objects}) {
my $errstr = validate_assembly($item->part,$self->part);
$self->js->flash('error',$errstr) if $errstr;
push (@checked_objects,$item) unless $errstr;
}

my $html = $self->render_assembly_items_to_html(\@checked_objects);

$self->js->run('kivi.Part.close_picker_dialogs')
->append('#assembly_rows', $html)
->run('kivi.Part.renumber_positions')
->run('kivi.Part.assembly_recalc')
->render();
}

sub action_add_assortment_item {
my ($self, %params) = @_;

validate_add_items() or return $self->js->error(t8("No part was selected."))->render;

carp('Too many objects passed to add_assortment_item') if @{$::form->{add_items}} > 1;

my $add_item_id = $::form->{add_items}->[0]->{parts_id};
if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assortment_items } ) {
return $self->js->flash('error', t8("This part has already been added."))->render;
};

my $number_of_items = scalar @{$self->assortment_items};
my $item_objects = $self->parse_add_items_to_objects(part_type => 'assortment');
my $html = $self->render_assortment_items_to_html($item_objects, $number_of_items);

push(@{$self->assortment_items}, @{$item_objects});
my $part = SL::DB::Part->new(part_type => 'assortment');
$part->assortment_items(@{$self->assortment_items});
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;

$self->js
->append('#assortment_rows' , $html) # append in tbody
->val('.add_assortment_item_input' , '')
->run('kivi.Part.focus_last_assortment_input')
->html("#items_sellprice_sum", $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
->html("#items_lastcost_sum", $::form->format_amount(\%::myconfig, $items_lastcost_sum, 2, 0))
->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))
->render;
}

sub action_add_assembly_item {
my ($self) = @_;

validate_add_items() or return $self->js->error(t8("No part was selected."))->render;

carp('Too many objects passed to add_assembly_item') if @{$::form->{add_items}} > 1;

my $add_item_id = $::form->{add_items}->[0]->{parts_id};

my $duplicate_warning = 0; # duplicates are allowed, just warn
if ( $add_item_id && grep { $add_item_id == $_->parts_id } @{ $self->assembly_items } ) {
$duplicate_warning++;
};

my $number_of_items = scalar @{$self->assembly_items};
my $item_objects = $self->parse_add_items_to_objects(part_type => 'assembly');
if ($add_item_id ) {
foreach my $item (@{$item_objects}) {
my $errstr = validate_assembly($item->part,$self->part);
return $self->js->flash('error',$errstr)->render if $errstr;
}
}


my $html = $self->render_assembly_items_to_html($item_objects, $number_of_items);

$self->js->flash('info', t8("This part has already been added.")) if $duplicate_warning;

push(@{$self->assembly_items}, @{$item_objects});
my $part = SL::DB::Part->new(part_type => 'assembly');
$part->assemblies(@{$self->assembly_items});
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
->val('.add_assembly_item_input' , '')
->run('kivi.Part.focus_last_assembly_input')
->html('#items_sellprice_sum', $::form->format_amount(\%::myconfig, $items_sellprice_sum, 2, 0))
->html('#items_lastcost_sum' , $::form->format_amount(\%::myconfig, $items_lastcost_sum , 2, 0))
->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,
search_term => $search_term
);
}

sub action_multi_items_update_result {
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 ($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;
$_[0]->render('part/_multi_items_result', { layout => 0 },
multi_items => $multi_items);
}
}

sub action_add_makemodel_row {
my ($self) = @_;

my $vendor_id = $::form->{add_makemodel};

my $vendor = SL::DB::Manager::Vendor->find_by(id => $vendor_id) or
return $self->js->error(t8("No vendor selected or found!"))->render;

if ( grep { $vendor_id == $_->make } @{ $self->makemodels } ) {
$self->js->flash('info', t8("This vendor has already been added."));
};

my $position = scalar @{$self->makemodels} + 1;

my $mm = SL::DB::MakeModel->new(# parts_id => $::form->{part}->{id},
make => $vendor->id,
model => '',
part_description => '',
part_longdescription => '',
lastcost => 0,
sortorder => $position,
) or die "Can't create MakeModel object";

my $row_as_html = $self->p->render('part/_makemodel_row',
makemodel => $mm,
listrow => $position % 2 ? 0 : 1,
);

# after selection focus on the model field in the row that was just added
$self->js
->append('#makemodel_rows', $row_as_html) # append in tbody
->val('.add_makemodel_input', '')
->run('kivi.Part.focus_last_makemodel_input')
->render;
}

sub action_add_businessmodel_row {
my ($self) = @_;

my $business_id = $::form->{add_businessmodel};

my $business = SL::DB::Manager::Business->find_by(id => $business_id) or
return $self->js->error(t8("No business selected or found!"))->render;

if ( grep { $business_id == $_->business_id } @{ $self->businessmodels } ) {
return $self->js
->scroll_into_view('#content')
->flash('error', (t8("This business has already been added.")))
->render;
};

my $position = scalar @{ $self->businessmodels } + 1;

my $bm = SL::DB::BusinessModel->new(#parts_id => $::form->{part}->{id},
business => $business,
model => '',
part_description => '',
part_longdescription => '',
position => $position,
) or die "Can't create BusinessModel object";

my $row_as_html = $self->p->render('part/_businessmodel_row',
businessmodel => $bm);

# after selection focus on the model field in the row that was just added
$self->js
->append('#businessmodel_rows', $row_as_html) # append in tbody
->val('#add_businessmodel', '')
->run('kivi.Part.focus_last_businessmodel_input')
->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 => '',
part_description => '',
part_longdescription => '',
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) = @_;

my $part_type = $::form->{part_type};

my %sort_keys = (
partnumber => sub { $_[0]->part->partnumber },
description => sub { $_[0]->part->description },
qty => sub { $_[0]->qty },
sellprice => sub { $_[0]->part->sellprice },
lastcost => sub { $_[0]->part->lastcost },
partsgroup => sub { $_[0]->part->partsgroup_id ? $_[0]->part->partsgroup->partsgroup : '' },
);

my $method = $sort_keys{$::form->{order_by}};

my @items;
if ($part_type eq 'assortment') {
@items = @{ $self->assortment_items };
} else {
@items = @{ $self->assembly_items };
};

my @to_sort = map { { old_pos => $_->position, order_by => $method->($_) } } @items;
if ($::form->{order_by} =~ /^(qty|sellprice|lastcost)$/) {
if ($::form->{sort_dir}) {
@to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort;
} else {
@to_sort = sort { $b->{order_by} <=> $a->{order_by} } @to_sort;
}
} else {
if ($::form->{sort_dir}) {
@to_sort = sort { $a->{order_by} cmp $b->{order_by} } @to_sort;
} else {
@to_sort = sort { $b->{order_by} cmp $a->{order_by} } @to_sort;
}
};

$self->js->run('kivi.Part.redisplay_items', \@to_sort)->render;
}

sub action_reorder_variants {
my ($self) = @_;

my $part= $self->part;

my %sort_keys = (
partnumber => sub { $_[0]->partnumber },
description => sub { $_[0]->description },
ean => sub { $_[0]->ean },
listprice => sub { $_[0]->listprice },
sellprice => sub { $_[0]->sellprice },
lastcost => sub { $_[0]->lastcost },
onhand => sub { $_[0]->onhand },
rop => sub { $_[0]->rop },
variant_values => sub { $_[0]->variant_values },
);
foreach my $variant_property (@{$part->variant_properties}) {
my $key = 'variant_property_' . $variant_property->id;
$sort_keys{$key} = sub {
$_[0]->get_variant_property_value_by_unique_name($variant_property->unique_name)->sortkey;
}
}

my $method = $sort_keys{$::form->{order_by}};

my @items = $part->variants;

my %variant_id_to_position =
map {$_->{id} => $_->{position}}
@{$::form->{variants}};

my @to_sort = map { { old_pos => $variant_id_to_position{$_->id}, order_by => $method->($_) } } @items;
if ($::form->{order_by} =~ /^(listpirce|sellprice|lastcost|onhand|rop)$/ ||
$::form->{order_by} =~ /^variant_property_/) {
if ($::form->{sort_dir}) {
@to_sort = sort { $a->{order_by} <=> $b->{order_by} } @to_sort
# lastupdate isn't set, original lastcost is 0 and new lastcost is 0
# don't change lastupdate
} elsif ( !$makemodels_map->{$mm->id} && $mm->lastcost == 0 ) {
# new makemodel, no lastcost entered, leave lastupdate empty
} elsif ($makemodels_map->{$mm->id} && $makemodels_map->{$mm->id}->lastcost == $mm->lastcost) {
# lastcost hasn't changed, use original lastupdate
$mm->lastupdate($makemodels_map->{$mm->id}->lastupdate);
} else {
$mm->lastupdate(DateTime->now);
};
$self->part->makemodel( scalar @{$self->part->makemodels} ? 1 : 0 ); # do we need this boolean anymore?
$self->part->add_makemodels($mm);
};
}

sub parse_form_businessmodels {
my ($self) = @_;

my $make_key = sub { return $_[0]->parts_id . '+' . $_[0]->business_id; };

my $businessmodels_map;
if ( $self->part->businessmodels ) { # check for new parts or parts without businessmodels
$businessmodels_map = { map { $make_key->($_) => Rose::DB::Object::Helpers::clone($_) } @{$self->part->businessmodels} };
};

$self->part->businessmodels([]);

my $position = 0;
my $businessmodels = delete($::form->{businessmodels}) || [];
foreach my $businessmodel ( @{$businessmodels} ) {
next unless $businessmodel->{business_id};

$position++;
my $bm = SL::DB::BusinessModel->new( #parts_id => $self->part->id, # will be assigned by row add_businessmodels
business_id => $businessmodel->{business_id},
model => $businessmodel->{model} || '',
part_description => $businessmodel->{part_description} || '',
part_longdescription => $businessmodel->{part_longdescription} || '',
position => $position,
);

$self->part->add_businessmodels($bm);
};
}

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} || '',
part_description => $customerprice->{part_description},
part_longdescription => $customerprice->{part_longdescription},
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 {
select_tag('part.bin_id', [ @{ $_[0]->warehouse->bins_sorted_naturally } ],
title_key => 'description',
default => $_[0]->bin->id,
);
}


# get_set_inits for partpicker

sub init_parts {
if ($::form->{no_paginate}) {
$_[0]->models->disable_plugin('paginated');
}

$_[0]->models->get;
}

# get_set_inits for part controller
sub init_part {
my ($self) = @_;

# used by edit, save, delete and add

if ( $::form->{part}{id} ) {
return SL::DB::Part->new(id => $::form->{part}{id})->load(with => [ qw(makemodels businessmodels 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});
};
}

sub init_orphaned {
my ($self) = @_;
return $self->part->orphaned;
}

sub init_models {
my ($self) = @_;

SL::Controller::Helper::GetModels->new(
controller => $self,
sorted => {
_default => {
by => 'partnumber',
dir => 1,
},
partnumber => t8('Partnumber'),
description => t8('Description'),
},
with_objects => [ qw(unit_obj classification) ],
);
}

sub init_p {
SL::Presenter->get;
}


sub init_assortment_items {
# this init is used while saving and whenever assortments change dynamically
my ($self) = @_;
my $position = 0;
my @array;
my $assortment_items = delete($::form->{assortment_items}) || [];
foreach my $assortment_item ( @{$assortment_items} ) {
next unless $assortment_item->{parts_id};
$position++;
my $part = SL::DB::Manager::Part->find_by(id => $assortment_item->{parts_id}) || die "Can't determine item to be added";
my $ai = SL::DB::AssortmentItem->new( parts_id => $part->id,
qty => $::form->parse_amount(\%::myconfig, $assortment_item->{qty_as_number}),
charge => $assortment_item->{charge},
unit => $assortment_item->{unit} || $part->unit,
position => $position,
);

push(@array, $ai);
};
return \@array;
}

sub init_makemodels {
my ($self) = @_;

my $position = 0;
my @makemodel_array = ();
my $makemodels = delete($::form->{makemodels}) || [];

foreach my $makemodel ( @{$makemodels} ) {
next unless $makemodel->{make};
$position++;
my $mm = SL::DB::MakeModel->new( # parts_id => $self->part->id, # will be assigned by row add_makemodels
id => $makemodel->{id},
make => $makemodel->{make},
model => $makemodel->{model} || '',
part_description => $makemodel->{part_description} || '',
part_longdescription => $makemodel->{part_longdescription} || '',
lastcost => $::form->parse_amount(\%::myconfig, $makemodel->{lastcost_as_number} || 0),
sortorder => $position,
) or die "Can't create mm";
# $mm->id($makemodel->{id}) if $makemodel->{id};
push(@makemodel_array, $mm);
};
return \@makemodel_array;
}

sub init_businessmodels {
my ($self) = @_;

my @businessmodel_array = ();
my $businessmodels = delete($::form->{businessmodels}) || [];

foreach my $businessmodel ( @{$businessmodels} ) {
next unless $businessmodel->{business_id};

my $bm = SL::DB::BusinessModel->new(#parts_id => $self->part->id, # will be assigned by row add_businessmodels
business_id => $businessmodel->{business_id},
model => $businessmodel->{model} || '',
part_description => $businessmodel->{part_description} || '',
part_longdescription => $businessmodel->{part_longdescription} || '',
) or die "Can't create bm";

push(@businessmodel_array, $bm);
};

return \@businessmodel_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} || '',
part_description => $customerprice->{part_description},
part_longdescription => $customerprice->{part_longdescription},
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;
my @array;
my $assembly_items = delete($::form->{assembly_items}) || [];
foreach my $assembly_item ( @{$assembly_items} ) {
next unless $assembly_item->{parts_id};
$position++;
my $part = SL::DB::Manager::Part->find_by(id => $assembly_item->{parts_id}) || die "Can't determine item to be added";
my $ai = SL::DB::Assembly->new(parts_id => $part->id,
bom => $assembly_item->{bom},
qty => $::form->parse_amount(\%::myconfig, $assembly_item->{qty_as_number}),
position => $position,
);
push(@array, $ai);
};
return \@array;
}

sub init_all_warehouses {
my ($self) = @_;
SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef, id => $self->part->warehouse_id ] ]);
}

sub init_all_languages {
SL::DB::Manager::Language->get_all_sorted;
}

sub init_all_partsgroups {
my ($self) = @_;
SL::DB::Manager::PartsGroup->get_all_sorted(query => [ or => [ id => $self->part->partsgroup_id, obsolete => 0 ] ]);
}

sub init_all_buchungsgruppen {
my ($self) = @_;
if (!$self->part->orphaned) {
return SL::DB::Manager::Buchungsgruppe->get_all_sorted(where => [ id => $self->part->buchungsgruppen_id ]);
}

return SL::DB::Manager::Buchungsgruppe->get_all_sorted(
where => [
or => [
id => $self->part->buchungsgruppen_id,
obsolete => 0,
],
]
);
}

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_units {
my ($self) = @_;
if ( $self->part->orphaned ) {
return SL::DB::Manager::Unit->get_all_sorted;
} else {
return SL::DB::Manager::Unit->get_all(where => [ unit => $self->part->unit ]);
}
}

sub init_all_payment_terms {
my ($self) = @_;
SL::DB::Manager::PaymentTerm->get_all_sorted(query => [ or => [ id => $self->part->payment_id, obsolete => 0 ] ]);
}

sub init_all_price_factors {
SL::DB::Manager::PriceFactor->get_all_sorted;
}

sub init_all_pricegroups {
SL::DB::Manager::Pricegroup->get_all_sorted(query => [ obsolete => 0 ]);
}

sub init_all_businesses {
SL::DB::Manager::Business->get_all_sorted;
}

# model used to filter/display the parts in the multi-items dialog
sub init_multi_items_models {
SL::Controller::Helper::GetModels->new(
controller => $_[0],
model => 'Part',
with_objects => [ qw(unit_obj partsgroup classification) ],
disable_plugin => 'paginated',
source => $::form->{multi_items},
sorted => {
_default => {
by => 'partnumber',
dir => 1,
},
partnumber => t8('Partnumber'),
description => t8('Description')}
);
}

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 {
my ($self) = @_;

return 1 if $::form->{part}{description};

$self->js->flash('error', t8('Part Description missing!'))
->run('kivi.Part.set_tab_active_by_name', 'basic_data')
->focus('#part_description');
return 0;
}

sub form_check_assortment_items_exist {
my ($self) = @_;

return 1 unless $::form->{part}{part_type} eq 'assortment';
# 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}
unless ( $::form->{assortment_items} and scalar @{$::form->{assortment_items}} ) {
$self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
->focus('#add_assortment_item_name')
->flash('error', t8('The assortment doesn\'t have any items.'));
return 0;
};
return 1;
}

sub form_check_assortment_items_unique {
my ($self) = @_;

return 1 unless $::form->{part}{part_type} eq 'assortment';

my %duplicate_elements;
my %count;
for (map { $_->{parts_id} } @{$::form->{assortment_items}}) {
$duplicate_elements{$_}++ if $count{$_}++;
};

if ( keys %duplicate_elements ) {
$self->js->run('kivi.Part.set_tab_active_by_name', 'assortment_tab')
->flash('error', t8('There are duplicate assortment items'));
return 0;
};
return 1;
}

sub form_check_assembly_items_exist {
my ($self) = @_;

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')
->flash('error', t8('The assembly doesn\'t have any items.'));
return 0;
};
return 1;
}

sub form_check_partnumber_is_unique {
my ($self) = @_;

if ( !$::form->{part}{id} and $::form->{part}{partnumber} ) {
my $count = SL::DB::Manager::Part->get_all_count(where => [ partnumber => $::form->{part}{partnumber} ]);
if ( $count ) {
$self->js->flash('error', t8('The partnumber already exists!'))
->focus('#part_description');
return 0;
};
};
return 1;
}

sub form_check_buchungsgruppe {
my ($self) = @_;

return 1 if $::form->{part}->{obsolete};

my $buchungsgruppe = SL::DB::Buchungsgruppe->new(id => $::form->{part}->{buchungsgruppen_id})->load;

return 1 if !$buchungsgruppe->obsolete;

$self->js->flash('error', t8("The booking group '#1' is obsolete and cannot be used with active articles.", $buchungsgruppe->description))
->focus('#part_buchungsgruppen_id');

return 0;
}

# general checking functions

sub check_part_id {
die t8("Can't load item without a valid part.id") . "\n" unless $::form->{part}{id};
}

sub check_form {
my ($self) = @_;

$self->form_check_part_description_exists || return 0;
$self->form_check_assortment_items_exist || return 0;
$self->form_check_assortment_items_unique || return 0;
$self->form_check_assembly_items_exist || return 0;
$self->form_check_partnumber_is_unique || return 0;
$self->form_check_buchungsgruppe || return 0;

return 1;
}

sub check_has_valid_part_type {
Carp::confess "invalid part_type" unless $_[0] =~ /^(part|service|assembly|assortment)$/;
}

sub _check_variant_property_values_not_taken {
my ($parent_variant, $variant_property_value_lists) = @_;

my @double_lists;
my $variants = $parent_variant->variants;
foreach my $variant (@$variants) {
my @property_value_ids = sort map {$_->id} $variant->variant_property_values;
foreach my $test_property_values (@$variant_property_value_lists) {
my @test_property_value_ids = sort map {$_->id} @$test_property_values;
if (@test_property_value_ids == @property_value_ids
&& not any {$_} pairwise {$a != $b} @test_property_value_ids, @property_value_ids
) {
push @double_lists, join(', ', map {$_->displayable_name} @$test_property_values);
}
}
}

if (@double_lists) {
die t8("There is already a variant with the property values: #1.", join("; ", @double_lists));
}
}

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>(&nbsp;)+\s+/<p>/s;
$self->part->{notes} =~ s/(&nbsp;)+<\/p>$/<\/p>/s;

}

sub render_assortment_items_to_html {
my ($self, $assortment_items, $number_of_items) = @_;

my $position = $number_of_items + 1;
my $html;
foreach my $ai (@$assortment_items) {
$html .= $self->p->render('part/_assortment_row',
PART => $self->part,
orphaned => $self->orphaned,
ITEM => $ai,
listrow => $position % 2 ? 1 : 0,
position => $position, # for legacy assemblies
);
$position++;
};
return $html;
}

sub render_assembly_items_to_html {
my ($self, $assembly_items, $number_of_items) = @_;

my $position = $number_of_items + 1;
my $html;
foreach my $ai (@{$assembly_items}) {
$html .= $self->p->render('part/_assembly_row',
PART => $self->part,
orphaned => $self->orphaned,
ITEM => $ai,
listrow => $position % 2 ? 1 : 0,
position => $position, # for legacy assemblies
);
$position++;
};
return $html;
}

sub parse_add_items_to_objects {
my ($self, %params) = @_;
my $part_type = $params{part_type};
die unless $params{part_type} =~ /^(assortment|assembly)$/;
my $position = $params{position} || 1;

my @add_items = grep { $_->{qty_as_number} } @{ $::form->{add_items} };

my @item_objects;
foreach my $item ( @add_items ) {
my $part = SL::DB::Manager::Part->find_by(id => $item->{parts_id}) || die "Can't load part";
my $ai;
if ( $part_type eq 'assortment' ) {
$ai = SL::DB::AssortmentItem->new(part => $part,
qty => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
unit => $part->unit, # TODO: $item->{unit} || $part->unit
position => $position,
) or die "Can't create AssortmentItem from item";
} elsif ( $part_type eq 'assembly' ) {
$ai = SL::DB::Assembly->new(parts_id => $part->id,
# id => $self->assembly->id, # will be set on save
qty => $::form->parse_amount(\%::myconfig, $item->{qty_as_number}),
bom => 0, # default when adding: no bom
position => $position,
);
} else {
die "part_type must be assortment or assembly";
}
push(@item_objects, $ai);
$position++;
};

return \@item_objects;
}

sub _is_in_purchase_basket {
my ( $self ) = @_;

return SL::DB::Manager::PurchaseBasketItem->get_all_count( query => [ part_id => $self->part->id ] );
}

sub _is_ordered {
my ( $self ) = @_;

return $self->part->get_ordered_qty( $self->part->id );
}

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.')
: $self->part->order_locked ? t8('This part should not be ordered any more.')
: undef,
only_if => !$::form->{inline_create},
],
],

combobox => [
action => [
t8('Export'),
only_if => $self->part->is_assembly || $self->part->is_assortment,
],
action => [
$self->part->is_assembly ? t8('Assembly items') : t8('Assortment items'),
submit => [ '#ic', { action => "Part/export_assembly_assortment_components" } ],
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 => $self->part->is_assembly || $self->part->is_assortment,
],
],

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,
],

action => [
t8('Add to basket'),
call => [ 'kivi.Part.add_to_basket' ],
disabled => !$self->part->id ? t8('This object has not been saved yet.')
: $self->_is_in_purchase_basket ? t8('Part already in purchasebasket')
: $self->_is_ordered ? t8('Part already ordered')
: !scalar @{$self->part->makemodels} ? t8('No vendors to add to purchasebasket')
: 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__

=encoding utf-8

=head1 NAME

SL::Controller::Part - Part CRUD controller

=head1 DESCRIPTION

Controller for adding/editing/saving/deleting parts.

All the relations are loaded at once and saving the part, adding a history
entry and saving CVars happens inside one transaction. When saving the old
relations are deleted and written as new to the database.

Relations for parts:

=over 2

=item makemodels

=item translations

=item assembly items

=item assortment items

=item prices

=back

=head1 PART_TYPES

There are 4 different part types:

=over 4

=item C<part>

The "default" part type.

inventory_accno_id is set.

=item C<service>

Services can't be stocked.

inventory_accno_id isn't set.

=item C<assembly>

Assemblies consist of other parts, services, assemblies or assortments. They
aren't meant to be bought, only sold. To add assemblies to stock you typically
have to make them, which reduces the stock by its respective components. Once
an assembly item has been created there is currently no way to "disassemble" it
again. An assembly item can appear several times in one assembly. An assmbly is
sold as one item with a defined sellprice and lastcost. If the component prices
change the assortment price remains the same. The assembly items may be printed
in a record if the item's "bom" is set.

=item C<assortment>

Similar to assembly, but each assortment item may only appear once per
assortment. When selling an assortment the assortment items are added to the
record together with the assortment, which is added with sellprice 0.

Technically an assortment doesn't have a sellprice, but rather the sellprice is
determined by the sum of the current assortment item prices when the assortment
is added to a record. This also means that price rules and customer discounts
will be applied to the assortment items.

Once the assortment items have been added they may be modified or deleted, just
as if they had been added manually, the individual assortment items aren't
linked to the assortment or the other assortment items in any way.

=back

=head1 URL ACTIONS

=over 4

=item C<action_add_part>

=item C<action_add_service>

=item C<action_add_assembly>

=item C<action_add_assortment>

=item C<action_add PART_TYPE>

An alternative to the action_add_$PART_TYPE actions, takes the mandatory
parameter part_type as an action. Example:

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.

=item C<action_use_as_new>

Takes the information from the current part, plus any modifications made on the
page, and creates a new edit page that is ready to be saved. The partnumber is
set empty, so a new partnumber from the number range will be used if the user
doesn't enter one manually.

Unsaved changes to the original part aren't updated.

The part type cannot be changed in this way.

=item C<action_delete>
=item *

It should be possible to jump to the edit page in a specific tab

=item *

Support callbacks, e.g. creating a new part from within an order, and jumping
back to the order again afterwards.

=item *

Support units when adding assembly items or assortment items. Currently the
default unit of the item is always used.

=item *

Calculate sellprice and lastcost totals recursively, in case e.g. an assembly
consists of other assemblies.

=back

=head1 AUTHOR

G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>

=cut
(48-48/86)