|
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 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::Helper::Flash;
|
|
use SL::Helper::CreatePDF qw(:all);
|
|
use SL::Helper::PrintOptions;
|
|
use SL::Helper::UserPreferences::PartPickerSearch;
|
|
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
|
|
print_options
|
|
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_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) = @_;
|
|
|
|
my $is_new = !$self->part->id;
|
|
|
|
$self->save_with_render_error() or return;
|
|
|
|
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_print {
|
|
my ($self) = @_;
|
|
|
|
$self->save_with_render_error() or return;
|
|
$self->js_reset_part_after_save();
|
|
$self->js->flash('info', t8('The item has been saved.'));
|
|
|
|
my $formname = $::form->{print_options}->{formname};
|
|
my $language = $::form->{print_options}->{language};
|
|
my $format = $::form->{print_options}->{format};
|
|
my $media = $::form->{print_options}->{media};
|
|
my $printer_id = $::form->{print_options}->{printer_id};
|
|
my $copies = $::form->{print_options}->{copies};
|
|
|
|
my %result;
|
|
eval {
|
|
%result = SL::Template::LaTeX->parse_and_create_pdf(
|
|
$formname . ".tex",
|
|
SELF => $self,
|
|
part => $self->part,
|
|
template_meta => {
|
|
formname => 'part',
|
|
language => $language,
|
|
extension => 'pdf',
|
|
format => $format,
|
|
media => $media,
|
|
today => DateTime->today,
|
|
lxconfig => \%::lx_office_conf,
|
|
},
|
|
);
|
|
if ($result{error}) {
|
|
die t8('Conversion to PDF failed: #1', $result{error});
|
|
}
|
|
|
|
my $pdf = $result{file_name};
|
|
|
|
if ($media eq 'screen') {
|
|
my $file_name = $formname . '.pdf';
|
|
$file_name =~ s{[^\w\.]+}{_}g;
|
|
|
|
$self->send_file(
|
|
$pdf,
|
|
type => 'application/pdf',
|
|
name => $file_name,
|
|
js_no_render => 1,
|
|
);
|
|
unlink $result{file_name};
|
|
} elsif ($media eq 'printer') {
|
|
my $printer = SL::DB::Printer->new(id => $printer_id)->load;
|
|
$printer->print_document(
|
|
copies => $copies,
|
|
file_name => $result{file_name},
|
|
);
|
|
|
|
$self->js->flash('info', t8('The document has been sent to the printer \'#1\'.', $printer->printer_description));
|
|
unlink $result{file_name} if $result{file_name};
|
|
} else {
|
|
die t8('Media \'#1\' is not supported yet/anymore.', $media);
|
|
}
|
|
|
|
1;
|
|
} or do {
|
|
unlink $result{file_name} if $result{file_name};
|
|
$self->js
|
|
->flash('error', t8("Creating the PDF failed!"))
|
|
->flash('error', $@);
|
|
};
|
|
|
|
$self->js->render();
|
|
}
|
|
|
|
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_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";
|
"Part.pm.html#L443" data-txt="443">
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 |