+__PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') },
+ except => [ qw(ajax_autocomplete part_picker_search part_picker_result) ]);
+
+__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_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;
+
+ # $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(
+ 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_as_new {
+ my ($self) = @_;
+ $self->action_save(save_as_new=>1);
+}
+
+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;
+ $self->part->partnumber(undef);
+
+ $self->render_form;
+}
+
+sub action_edit {
+ my ($self, %params) = @_;
+
+ $self->render_form;
+}
+
+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;
+ %assembly_vars = %{ $self->prepare_assembly_render_vars } if $self->part->is_assembly;
+
+ $params{CUSTOM_VARIABLES} = CVar->get_custom_variables(module => 'IC', trans_id => $self->part->id);
+
+ 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'),
+ );
+
+ $self->part->prices([]) unless $self->part->prices;
+ $self->part->translations([]) unless $self->part->translations;
+
+ $self->render(
+ 'part/form',
+ title => $title_hash{$self->part->part_type},
+ %assortment_vars,
+ %assembly_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 $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))
+ ->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;
+
+ $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))
+ ->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 => '',
+ 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_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) = @_;
+
+ 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_warehouse_changed {
+ my ($self) = @_;
+
+ if ($::form->{warehouse_id} ) {
+ $self->warehouse(SL::DB::Manager::Warehouse->find_by_or_create(id => $::form->{warehouse_id}));
+ die unless ref($self->warehouse) eq 'SL::DB::Warehouse';
+
+ if ( $self->warehouse->id and @{$self->warehouse->bins} ) {
+ $self->bin($self->warehouse->bins_sorted->[0]);
+ $self->js
+ ->html('#bin', $self->build_bin_select)
+ ->focus('#part_bin_id');
+ return $self->js->render;
+ }
+ }
+
+ # no warehouse was selected, empty the bin field and reset the id
+ $self->js
+ ->val('#part_bin_id', undef)
+ ->html('#bin', '');
+
+ return $self->js->render;
+}