Revision 52518527
Von Martin Helmling martin.helmling@octosoft.eu vor mehr als 8 Jahren hinzugefügt
SL/Controller/CsvImport/Part.pm | ||
---|---|---|
(
|
||
scalar => [ qw(table makemodel_columns) ],
|
||
'scalar --get_set_init' => [ qw(bg_by settings parts_by price_factors_by units_by partsgroups_by
|
||
warehouses_by bins_by
|
||
translation_columns all_pricegroups) ],
|
||
);
|
||
|
||
... | ... | |
return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
|
||
}
|
||
|
||
sub init_bins_by {
|
||
my ($self) = @_;
|
||
|
||
my $all_bins = SL::DB::Manager::Bin->get_all;
|
||
return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_bins } } ) } qw(id description) };
|
||
}
|
||
|
||
sub init_warehouses_by {
|
||
my ($self) = @_;
|
||
|
||
my $all_warehouses = SL::DB::Manager::Warehouse->get_all;
|
||
return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
|
||
}
|
||
|
||
|
||
sub init_parts_by {
|
||
my ($self) = @_;
|
||
|
||
... | ... | |
$self->check_price_factor($entry);
|
||
$self->check_payment($entry);
|
||
$self->check_partsgroup($entry);
|
||
$self->check_warehouse_and_bin($entry);
|
||
$self->handle_pricegroups($entry);
|
||
$self->check_existing($entry) unless @{ $entry->{errors} };
|
||
$self->handle_prices($entry) if $self->settings->{sellprice_adjustment};
|
||
... | ... | |
} continue {
|
||
$i++;
|
||
}
|
||
|
||
$self->add_columns(qw(type)) if $self->settings->{parts_type} eq 'mixed';
|
||
$self->add_columns(qw(buchungsgruppen_id unit));
|
||
$self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup));
|
||
$self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup warehouse bin));
|
||
$self->add_columns(qw(shop)) if $self->settings->{shoparticle_if_missing};
|
||
$self->add_cvar_raw_data_columns;
|
||
map { $self->add_raw_data_columns("pricegroup_${_}") if exists $self->controller->data->[0]->{raw_data}->{"pricegroup_$_"} } (1..scalar(@{ $self->all_pricegroups }));
|
||
... | ... | |
$default_id = undef unless $self->bg_by->{id}->{ $default_id };
|
||
|
||
# 1. Use default ID if enforced.
|
||
$object->buchungsgruppen_id($default_id) if $default_id && ($self->settings->{apply_buchungsgruppe} eq 'all');
|
||
if ($default_id && ($self->settings->{apply_buchungsgruppe} eq 'all')) {
|
||
$object->buchungsgruppen_id($default_id);
|
||
push @{ $entry->{information} }, $::locale->text('Use default booking group because setting is \'all\'');
|
||
}
|
||
|
||
# 2. Use supplied ID if valid
|
||
$object->buchungsgruppen_id(undef) if $object->buchungsgruppen_id && !$self->bg_by->{id}->{ $object->buchungsgruppen_id };
|
||
... | ... | |
}
|
||
|
||
# 4. Use default ID if not valid.
|
||
$object->buchungsgruppen_id($default_id) if !$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing');
|
||
|
||
if (!$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing')) {
|
||
$object->buchungsgruppen_id($default_id) ;
|
||
$entry->{buch_information} = $::locale->text('Use default booking group because wanted is missing');
|
||
}
|
||
return 1 if $object->buchungsgruppen_id;
|
||
$entry->{buch_error} = $::locale->text('Error: booking group missing or invalid');
|
||
return 0;
|
||
}
|
||
|
||
push @{ $entry->{errors} }, $::locale->text('Error: booking group missing or invalid');
|
||
sub _part_is_used {
|
||
my ($self, $part) = @_;
|
||
|
||
my $query =
|
||
qq|SELECT COUNT(parts_id) FROM invoice where parts_id = ?
|
||
UNION
|
||
SELECT COUNT(parts_id) FROM assembly where parts_id = ?
|
||
UNION
|
||
SELECT COUNT(parts_id) FROM orderitems where parts_id = ?
|
||
|;
|
||
foreach my $ref (selectall_hashref_query($::form, $part->db->dbh, $query, $part->id, $part->id, $part->id)) {
|
||
return 1 if $ref->{count} != 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
... | ... | |
my ($self, $entry) = @_;
|
||
|
||
my $object = $entry->{object};
|
||
my $raw = $entry->{raw_data};
|
||
|
||
if ($object->partnumber && $self->parts_by->{partnumber}{$object->partnumber}) {
|
||
$entry->{part} = SL::DB::Manager::Part->find_by(partnumber => $object->partnumber);
|
||
$entry->{part} = SL::DB::Manager::Part->get_all( query => [ partnumber => $object->partnumber ], limit => 1,
|
||
with_objects => [ 'translations', 'custom_variables' ]
|
||
) -> [0];
|
||
if ( !$entry->{part} ) {
|
||
$entry->{part} = SL::DB::Manager::Part->get_all( query => [ partnumber => $object->partnumber ], limit => 1,
|
||
with_objects => [ 'translations' ]
|
||
) -> [0];
|
||
}
|
||
}
|
||
|
||
if ($entry->{part}) {
|
||
if ($self->settings->{article_number_policy} eq 'update_prices') {
|
||
if ($self->settings->{parts_type} eq 'mixed' && $entry->{part}->type ne $object->type) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
|
||
} else {
|
||
map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
|
||
if ($entry->{part}->type ne $object->type ) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
|
||
return;
|
||
}
|
||
if ( $entry->{part}->unit != $object->unit || $entry->{part}->inventory_accno_id != $object->inventory_accno_id ) {
|
||
if ( $entry->{part}->onhand != 0 || $self->_part_is_used($entry->{part})) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry with different unit or inventory_accno_id'));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($self->settings->{article_number_policy} eq 'update_prices_sn' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
|
||
if (!$entry->{part}) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping non-existent article'));
|
||
return;
|
||
}
|
||
}
|
||
|
||
## checking also doubles in csv !!
|
||
foreach my $csventry (@{ $self->controller->data }) {
|
||
if ( $entry != $csventry && $object->partnumber eq $csventry->{object}->partnumber ) {
|
||
if ( $csventry->{doublechecked} ) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to same partnumber in csv file'));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
$entry->{doublechecked} = 1;
|
||
|
||
if ($entry->{part}) {
|
||
if ($self->settings->{article_number_policy} eq 'update_prices' || $self->settings->{article_number_policy} eq 'update_prices_sn') {
|
||
map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
|
||
|
||
# merge prices
|
||
my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
|
||
$entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
|
||
|
||
push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
|
||
$entry->{object_to_save} = $entry->{part};
|
||
} elsif ( $self->settings->{article_number_policy} eq 'update_parts' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
|
||
|
||
# Update parts table
|
||
# copy only the data which is not explicit copied by "methods"
|
||
|
||
map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(description notes weight ean rop image
|
||
drawing ve gv
|
||
unit
|
||
has_sernumber not_discountable obsolete
|
||
payment_id
|
||
sellprice listprice lastcost);
|
||
|
||
if (defined $raw->{"sellprice"} || defined $raw->{"listprice"} || defined $raw->{"lastcost"}) {
|
||
# merge prices
|
||
my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
|
||
$entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
|
||
}
|
||
|
||
push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
|
||
$entry->{object_to_save} = $entry->{part};
|
||
# Update translation
|
||
my @translations;
|
||
push @translations, $entry->{part}->translations;
|
||
foreach my $language (@{ $self->all_languages }) {
|
||
my $desc;
|
||
$desc = $raw->{"description_". $language->article_code} if defined $raw->{"description_". $language->article_code};
|
||
my $notes;
|
||
$notes = $raw->{"notes_". $language->article_code} if defined $raw->{"notes_". $language->article_code};
|
||
next unless $desc || $notes;
|
||
|
||
push @translations, SL::DB::Translation->new(language_id => $language->id,
|
||
translation => $desc,
|
||
longdescription => $notes);
|
||
}
|
||
} elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database'));
|
||
$entry->{part}->translations(\@translations) if @translations;
|
||
|
||
# Update cvars
|
||
my %type_to_column = ( text => 'text_value',
|
||
textfield => 'text_value',
|
||
select => 'text_value',
|
||
date => 'timestamp_value_as_date',
|
||
timestamp => 'timestamp_value_as_date',
|
||
number => 'number_value_as_number',
|
||
bool => 'bool_value' );
|
||
my @cvars;
|
||
push @cvars, $entry->{part}->custom_variables;
|
||
foreach my $config (@{ $self->all_cvar_configs }) {
|
||
next unless exists $raw->{ "cvar_" . $config->name };
|
||
my $value = $raw->{ "cvar_" . $config->name };
|
||
my $column = $type_to_column{ $config->type } || die "Program logic error: unknown custom variable storage type";
|
||
push @cvars, SL::DB::CustomVariable->new(config_id => $config->id, $column => $value, sub_module => '');
|
||
}
|
||
$entry->{part}->custom_variables(\@cvars) if @cvars;
|
||
|
||
# save Part Update
|
||
push @{ $entry->{information} }, $::locale->text('Updating data of existing entry in database');
|
||
|
||
$entry->{object_to_save} = $entry->{part};
|
||
# copy all other data via "methods"
|
||
my $methods = $self->controller->headers->{methods};
|
||
$entry->{object_to_save}->$_( $entry->{object}->$_ ) for @{ $methods }, keys %{ $self->clone_methods };
|
||
|
||
} elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database')) if ( $entry->{part} );
|
||
} else {
|
||
$object->partnumber('####');
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
|
||
#$object->partnumber('####');
|
||
}
|
||
} else {
|
||
push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
|
||
# set error or info from buch if part not exists
|
||
push @{ $entry->{information} }, $entry->{buch_information} if $entry->{buch_information};
|
||
push @{ $entry->{errors} }, $entry->{buch_error} if $entry->{buch_error};
|
||
}
|
||
}
|
||
|
||
... | ... | |
sub check_type {
|
||
my ($self, $entry) = @_;
|
||
|
||
my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
|
||
$bg ||= SL::DB::Buchungsgruppe->new(inventory_accno_id => 1); # does this case ever occur?
|
||
|
||
my $type = $self->settings->{parts_type};
|
||
if ($type eq 'mixed') {
|
||
|
||
if ($type eq 'mixed' && $entry->{raw_data}->{type}) {
|
||
$type = $entry->{raw_data}->{type} =~ m/^p/i ? 'part'
|
||
: $entry->{raw_data}->{type} =~ m/^s/i ? 'service'
|
||
: $entry->{raw_data}->{type} =~ m/^a/i ? 'assembly'
|
||
: undef;
|
||
}
|
||
|
||
$entry->{object}->assembly($type eq 'assembly');
|
||
|
||
# when saving income_accno_id or expense_accno_id use ids from the selected
|
||
# $bg according to the default tax_zone (the one with the highest sort
|
||
# order). Alternatively one could use the ids from defaults, but they might
|
||
# not all be set.
|
||
# Only use existing bg
|
||
|
||
$entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
|
||
my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
|
||
|
||
if ($type eq 'part' || $type eq 'service') {
|
||
$entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
|
||
}
|
||
# if not set there is an error occurred in check_buchungsgruppe()
|
||
# but if the part exists the new values for accno are ignored
|
||
|
||
if ($type eq 'part') {
|
||
$entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
|
||
}
|
||
if ( $bg ) {
|
||
$entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
|
||
$self->clone_methods->{income_accno_id} = 1;
|
||
|
||
if (none { $_ eq $type } qw(part service assembly)) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Invalid part type');
|
||
return 0;
|
||
if ($type eq 'part' || $type eq 'service') {
|
||
$entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
|
||
$self->clone_methods->{expense_accno_id} = 1;
|
||
}
|
||
}
|
||
|
||
if ($type eq 'part') {
|
||
if ( $bg ) {
|
||
$entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
|
||
}
|
||
else {
|
||
#use an existent bg
|
||
$entry->{object}->inventory_accno_id( SL::DB::Manager::Buchungsgruppe->get_first->id );
|
||
}
|
||
} elsif ($type eq 'assembly') {
|
||
$entry->{object}->assembly(1);
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
... | ... | |
}
|
||
|
||
$object->price_factor_id($pf->id);
|
||
$self->clone_methods->{price_factor_id} = 1;
|
||
}
|
||
|
||
return 1;
|
||
}
|
||
|
||
sub check_warehouse_and_bin {
|
||
my ($self, $entry) = @_;
|
||
|
||
my $object = $entry->{object};
|
||
|
||
# Check whether or not warehouse id is valid.
|
||
if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse id');
|
||
return 0;
|
||
}
|
||
# Map name to ID if given.
|
||
if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
|
||
my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
|
||
|
||
if (!$wh) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse name #1',$entry->{raw_data}->{warehouse});
|
||
return 0;
|
||
}
|
||
|
||
$object->warehouse_id($wh->id);
|
||
}
|
||
$self->clone_methods->{warehouse_id} = 1;
|
||
|
||
# Check whether or not bin id is valid.
|
||
if ($object->bin_id && !$self->bins_by->{id}->{ $object->bin_id }) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin id');
|
||
return 0;
|
||
}
|
||
# Map name to ID if given.
|
||
if ($object->warehouse_id && !$object->bin_id && $entry->{raw_data}->{bin}) {
|
||
my $bin = $self->bins_by->{description}->{ $entry->{raw_data}->{bin} };
|
||
|
||
if (!$bin) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin name #1',$entry->{raw_data}->{bin});
|
||
return 0;
|
||
}
|
||
|
||
$object->bin_id($bin->id);
|
||
}
|
||
$self->clone_methods->{bin_id} = 1;
|
||
|
||
if ($object->warehouse_id && $object->bin_id ) {
|
||
my $bin = $self->bins_by->{id}->{ $object->bin_id };
|
||
if ( $bin->warehouse_id != $object->warehouse_id ) {
|
||
push @{ $entry->{errors} }, $::locale->text('Error: Bin #1 is not from warehouse #2',
|
||
$self->bins_by->{id}->{$object->bin_id}->description,
|
||
$self->warehouses_by->{id}->{ $object->warehouse_id }->description);
|
||
return 0;
|
||
}
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
... | ... | |
|
||
$object->partsgroup_id($pg->id);
|
||
}
|
||
# register payment_id for method copying later
|
||
$self->clone_methods->{partsgroup_id} = 1;
|
||
|
||
return 1;
|
||
}
|
||
... | ... | |
my ($self) = @_;
|
||
|
||
my $profile = $self->SUPER::init_profile;
|
||
delete @{$profile}{qw(assembly bom expense_accno_id income_accno_id inventory_accno_id makemodel priceupdate stockable type)};
|
||
delete @{$profile}{qw(alternate assembly bom expense_accno_id income_accno_id inventory_accno_id makemodel priceupdate stockable type)};
|
||
|
||
$profile->{"pricegroup_$_"} = '' for 1 .. scalar @{ $_[0]->all_pricegroups };
|
||
|
||
... | ... | |
$self->SUPER::setup_displayable_columns;
|
||
$self->add_cvar_columns_to_displayable_columns;
|
||
|
||
$self->add_displayable_columns({ name => 'bin', description => $::locale->text('Bin') },
|
||
{ name => 'buchungsgruppen_id', description => $::locale->text('Booking group (database ID)') },
|
||
{ name => 'buchungsgruppe', description => $::locale->text('Booking group (name)') },
|
||
$self->add_displayable_columns({ name => 'assembly', description => $::locale->text('assembly') },
|
||
{ name => 'bin_id', description => $::locale->text('Bin (database ID)') },
|
||
{ name => 'bin', description => $::locale->text('Bin (name)') },
|
||
{ name => 'buchungsgruppen_id', description => $::locale->text('Booking group (database ID)') },
|
||
{ name => 'buchungsgruppe', description => $::locale->text('Booking group (name)') },
|
||
{ name => 'description', description => $::locale->text('Description') },
|
||
{ name => 'drawing', description => $::locale->text('Drawing') },
|
||
{ name => 'ean', description => $::locale->text('EAN') },
|
||
... | ... | |
{ name => 'gv', description => $::locale->text('Business Volume') },
|
||
{ name => 'has_sernumber', description => $::locale->text('Has serial number') },
|
||
{ name => 'image', description => $::locale->text('Image') },
|
||
{ name => 'inventory_accno_id', description => $::locale->text('part') },
|
||
{ name => 'lastcost', description => $::locale->text('Last Cost') },
|
||
{ name => 'listprice', description => $::locale->text('List Price') },
|
||
{ name => 'make_X', description => $::locale->text('Make (vendor\'s database ID, number or name; with X being a number)') . ' [1]' },
|
||
... | ... | |
{ name => 'price_factor', description => $::locale->text('Price factor (name)') },
|
||
{ name => 'rop', description => $::locale->text('ROP') },
|
||
{ name => 'sellprice', description => $::locale->text('Sellprice') },
|
||
{ name => 'shop', description => $::locale->text('Shop article') },
|
||
{ name => 'shop', description => $::locale->text('Shop article') },
|
||
{ name => 'type', description => $::locale->text('Article type') . ' [3]' },
|
||
{ name => 'unit', description => $::locale->text('Unit (if missing or empty default unit will be used)') },
|
||
{ name => 've', description => $::locale->text('Verrechnungseinheit') },
|
||
{ name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
|
||
{ name => 'warehouse', description => $::locale->text('Warehouse (name)') },
|
||
{ name => 'weight', description => $::locale->text('Weight') },
|
||
);
|
||
|
doc/changelog | ||
---|---|---|
|
||
- Für UStVA Voranmeldung über Elster gibt es die Anbindung über Geierlein (Installation/Config siehe Commit)
|
||
|
||
- CSV-Import von Artikel hat nun für existierende Artikel folgende Optionen:
|
||
1. Eigenschaften von existierenden Einträgen aktualisieren
|
||
2. Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen
|
||
3. Preise von vorhandenen Artikeln aktualisieren
|
||
4. Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen
|
||
5. Mit neuer Artikelnummer einfügen
|
||
6. Eintrag überspringen
|
||
Zusätzlich können nun Spalten "Lager","Lagerort" als Name oder ID eingelesen werden,
|
||
sowie Übersetzungen z.B. als 'description_EN' oder 'description_IT'.
|
||
Auch cvars können als 'cvars_<name>' importiert werden.
|
||
Ebenfalls sind zusätzliche Bemerkungen an den einzelnen Importzeilen eingebaut.
|
||
|
||
- In der Lager-Mandantenkonfig gibt es das Feature "Zum Fertigen Standardlager des Bestandteils verwenden".
|
||
Statt das Ziellager des Erzeugnisses zu Verwenden, wird nun zur Prüfung der Fertigung das
|
||
Standardlager der einzelnen Bestandteile verwendet.
|
locale/de/all | ||
---|---|---|
'Billing/shipping address (zipcode)' => 'Rechnungs-/Lieferadresse (PLZ)',
|
||
'Bin' => 'Lagerplatz',
|
||
'Bin (database ID)' => 'Lagerplatz (Datenbank-ID)',
|
||
'Bin (name)' => 'Lagerplatz (Name)',
|
||
'Bin From' => 'Quelllagerplatz',
|
||
'Bin List' => 'Lagerliste',
|
||
'Bin To' => 'Ziellagerplatz',
|
||
... | ... | |
'Booking group #1 needs a valid expense account' => 'Buchungsgruppe #1 braucht ein gültiges Aufwandskonto',
|
||
'Booking group #1 needs a valid income account' => 'Buchungsgruppe #1 braucht ein gültiges Erfolgskonto',
|
||
'Booking group #1 needs a valid inventory account' => 'Buchungsgruppe #1 braucht ein gültiges Warenbestandskonto',
|
||
'Booking group (database ID)' => 'Buchungsgruppe (Datenbank-ID)',
|
||
'Booking group (name)' => 'Buchungsgruppe (Name)',
|
||
'Booking group (database ID)' => 'Buchungsgruppe (database ID)',
|
||
'Booking group (name)' => 'Buchungsgruppe (name)',
|
||
'Booking groups' => 'Buchungsgruppen',
|
||
'Books are open' => 'Die Bücher sind geöffnet.',
|
||
'Books closed up to' => 'Bücher abgeschlossen bis zum',
|
||
... | ... | |
'Error: A negative target quantity is not allowed.' => 'Fehler: Eine negative Zielmenge ist nicht erlaubt.',
|
||
'Error: A quantity and a target quantity could not be given both.' => 'Fehler: Menge und Zielmenge können nicht beide angegeben werden.',
|
||
'Error: A quantity or a target quantity must be given.' => 'Fehler: Menge oder Zielmenge muss angegeben werden.',
|
||
'Error: Bin #1 is not from warehouse #2' => 'Lager \'#2\' hat keinen Lagerplatz \'#1\'',
|
||
'Error: Bin not found' => 'Fehler: Lagerplatz nicht gefunden',
|
||
'Error: Customer/vendor missing' => 'Fehler: Kunde/Lieferant fehlt',
|
||
'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
|
||
'Error: Found local bank account number but local bank code doesn\'t match' => 'Fehler: Kontonummer wurde gefunden aber gespeicherte Bankleitzahl stimmt nicht überein',
|
||
'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
|
||
'Error: Invalid bin' => 'Fehler: Ungültiger Lagerplatz',
|
||
'Error: Invalid bin id' => 'Ungültige Lagerplatz-ID',
|
||
'Error: Invalid bin name #1' => 'Ungültiger Lagerplatz \'#1\'',
|
||
'Error: Invalid business' => 'Fehler: Kunden-/Lieferantentyp ungültig',
|
||
'Error: Invalid contact' => 'Fehler: Ansprechperson ungültig',
|
||
'Error: Invalid currency' => 'Fehler: ungültige Währung',
|
||
... | ... | |
'Error: Invalid language' => 'Fehler: Sprache ungültig',
|
||
'Error: Invalid order for this order item' => 'Fehler: Auftrag für diese Position ungültig',
|
||
'Error: Invalid part' => 'Fehler: Artikel ungültig',
|
||
'Error: Invalid part type' => 'Fehler: Artikeltyp ungültig',
|
||
'Error: Invalid parts group' => 'Fehler: Warengruppe ungültig',
|
||
'Error: Invalid payment terms' => 'Fehler: Zahlungsbedingungen ungültig',
|
||
'Error: Invalid price factor' => 'Fehler: Preisfaktor ungültig',
|
||
... | ... | |
'Error: Invalid unit' => 'Fehler: Einheit ungültig',
|
||
'Error: Invalid vendor in column make_#1' => 'Fehler: Lieferant ungültig in Spalte make_#1',
|
||
'Error: Invalid warehouse' => 'Fehler: Ungültiges Lager',
|
||
'Error: Invalid warehouse id' => 'Ungültige Lager-ID',
|
||
'Error: Invalid warehouse name #1' => 'Ungültiger Lagername \'#1\'',
|
||
'Error: Name missing' => 'Fehler: Name fehlt',
|
||
'Error: Part not found' => 'Fehler: Artikel nicht gefunden',
|
||
'Error: Quantity to transfer is zero.' => 'Fehler: Zu bewegende Menge ist Null.',
|
||
... | ... | |
'Skipping due to existing bank transaction in database' => 'Wegen schon existierender Bankbewegung in Datenbank übersprungen',
|
||
'Skipping due to existing entry in database' => 'Wegen existierendem Eintrag mit selber Nummer übersprungen',
|
||
'Skipping due to existing entry in database with different type' => 'Wegen existierendem Eintrag von unterschiedlichem Artikeltyp übersprungen',
|
||
'Skipping, for assemblies are not importable (yet)' => 'Übersprungen, da Erzeugnisse (noch) nicht importiert werden können',
|
||
'Skipping due to existing entry with different unit or inventory_accno_id' => 'Wegen existierendem und verwendetem Eintrag von unterschiedlicher Einheit oder Buchungsgruppe übersprungen',
|
||
'Skipping due to same partnumber in csv file' => 'Eintrag in Datei mit doppelter Artikelnummer wird übersprungen',
|
||
'Skipping non-existent article' => 'Überspringe nicht vorhandenen Artikel',
|
||
'Skonto' => 'Skonto',
|
||
'Skonto Terms' => 'Zahlungsziel Skonto',
|
||
'Skonto amount' => 'Skontobetrag',
|
||
... | ... | |
'Update SKR04: new tax account 3804 (19%)' => 'Update SKR04: neues Steuerkonto 3804 (19%) für innergemeinschaftlichen Erwerb',
|
||
'Update prices' => 'Preise aktualisieren',
|
||
'Update prices of existing entries' => 'Preise von vorhandenen Artikeln aktualisieren',
|
||
'Update prices of existing entries / skip non-existent' => 'Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen',
|
||
'Update properties of existing entries' => 'Eigenschaften von existierenden Einträgen aktualisieren',
|
||
'Update properties of existing entries / skip non-existent' => 'Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen',
|
||
'Update quotation/order' => 'Auftrag/Angebot aktualisieren',
|
||
'Update sales order #1' => 'Kundenauftrag #1 aktualisieren',
|
||
'Update sales quotation #1' => 'Angebot #1 aktualisieren',
|
||
'Update this draft.' => 'Aktuellen Entwurf speichern',
|
||
'Update with section' => 'Mit Abschnitt aktualisieren',
|
||
'Updated' => 'Erneuert am',
|
||
'Updating data of existing entry in database' => 'Aktualisierung von vorhandenen Datenbankdaten',
|
||
'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
|
||
'Updating items with additional parts' => 'Positionen für zusätzliche Artikel aktualisieren',
|
||
'Updating items with sections' => 'Positionen für Abschnitte aktualisieren',
|
||
... | ... | |
'Use Income' => 'GUV und BWA verwenden',
|
||
'Use UStVA' => 'UStVA verwenden',
|
||
'Use WebDAV Repository' => 'WebDAV-Ablage verwenden',
|
||
'Use default booking group because setting is \'all\'' => 'Standardbuchungsgruppe wird verwendet',
|
||
'Use default booking group because wanted is missing' => 'Fehlende Buchungsgruppe, deshalb Standardbuchungsgruppe',
|
||
'Use default warehouse for assembly transfer' => 'Zum Fertigen Standardlager des Bestandteils verwenden',
|
||
'Use existing templates' => 'Vorhandene Druckvorlagen verwenden',
|
||
'Use linked items' => 'Verknüpfte Positionen verwenden',
|
||
... | ... | |
'WHJournal' => 'Lagerbuchungen',
|
||
'Warehouse' => 'Lager',
|
||
'Warehouse (database ID)' => 'Lager (Datenbank-ID)',
|
||
'Warehouse (name)' => 'Lager (Name)',
|
||
'Warehouse From' => 'Quelllager',
|
||
'Warehouse Migration' => 'Lagermigration',
|
||
'Warehouse To' => 'Ziellager',
|
t/controllers/csvimport/parts.t | ||
---|---|---|
use Test::More tests => 33;
|
||
|
||
use strict;
|
||
|
||
use lib 't';
|
||
|
||
use Carp;
|
||
use Data::Dumper;
|
||
use Support::TestSetup;
|
||
use Test::Exception;
|
||
|
||
use List::MoreUtils qw(pairwise);
|
||
use SL::Controller::CsvImport;
|
||
|
||
my $DEBUG = 0;
|
||
|
||
use_ok 'SL::Controller::CsvImport::Part';
|
||
|
||
use SL::DB::Buchungsgruppe;
|
||
use SL::DB::Currency;
|
||
use SL::DB::Customer;
|
||
use SL::DB::Language;
|
||
use SL::DB::Warehouse;
|
||
use SL::DB::Bin;
|
||
|
||
my ($translation, $bin1_1, $bin1_2, $bin2_1, $bin2_2, $wh1, $wh2, $bugru, $cvarconfig );
|
||
|
||
Support::TestSetup::login();
|
||
|
||
sub reset_state {
|
||
# Create test data
|
||
|
||
clear_up();
|
||
|
||
$translation = SL::DB::Language->new(
|
||
description => 'Englisch',
|
||
article_code => 'EN',
|
||
template_code => 'EN',
|
||
)->save;
|
||
$translation = SL::DB::Language->new(
|
||
description => 'Italienisch',
|
||
article_code => 'IT',
|
||
template_code => 'IT',
|
||
)->save;
|
||
$wh1 = SL::DB::Warehouse->new(
|
||
description => 'Lager1',
|
||
sortkey => 1,
|
||
)->save;
|
||
$bin1_1 = SL::DB::Bin->new(
|
||
description => 'Ort1_von_Lager1',
|
||
warehouse_id => $wh1->id,
|
||
)->save;
|
||
$bin1_2 = SL::DB::Bin->new(
|
||
description => 'Ort2_von_Lager1',
|
||
warehouse_id => $wh1->id,
|
||
)->save;
|
||
$wh2 = SL::DB::Warehouse->new(
|
||
description => 'Lager2',
|
||
sortkey => 2,
|
||
)->save;
|
||
$bin2_1 = SL::DB::Bin->new(
|
||
description => 'Ort1_von_Lager2',
|
||
warehouse_id => $wh2->id,
|
||
)->save;
|
||
$bin2_2 = SL::DB::Bin->new(
|
||
description => 'Ort2_von_Lager2',
|
||
warehouse_id => $wh2->id,
|
||
)->save;
|
||
|
||
$cvarconfig = SL::DB::CustomVariableConfig->new(
|
||
module => 'IC',
|
||
name => 'mycvar',
|
||
type => 'text',
|
||
description => 'mein schattz',
|
||
searchable => 1,
|
||
sortkey => 1,
|
||
includeable => 0,
|
||
included_by_default => 0,
|
||
)->save;
|
||
}
|
||
|
||
$bugru = SL::DB::Manager::Buchungsgruppe->find_by(description => { like => 'Standard%19%' });
|
||
|
||
reset_state();
|
||
|
||
#####
|
||
sub test_import {
|
||
my ($file,$settings) = @_;
|
||
my @profiles;
|
||
my $controller = SL::Controller::CsvImport->new();
|
||
|
||
my $csv_part_import = SL::Controller::CsvImport::Part->new(
|
||
settings => $settings,
|
||
controller => $controller,
|
||
file => $file,
|
||
);
|
||
|
||
$csv_part_import->init_bg_by;
|
||
$csv_part_import->init_price_factors_by;
|
||
$csv_part_import->init_partsgroups_by;
|
||
$csv_part_import->init_units_by;
|
||
$csv_part_import->init_bins_by;
|
||
$csv_part_import->init_warehouses_by;
|
||
$csv_part_import->init_parts_by;
|
||
$csv_part_import->test_run(0);
|
||
$csv_part_import->csv(SL::Helper::Csv->new(file => $csv_part_import->file,
|
||
profile => [{ profile => $csv_part_import->profile,
|
||
class => $csv_part_import->class,
|
||
mapping => $csv_part_import->controller->mappings_for_profile }],
|
||
encoding => 'utf-8',
|
||
ignore_unknown_columns => 1,
|
||
strict_profile => 1,
|
||
case_insensitive_header => 1,
|
||
sep_char => ';',
|
||
quote_char => '"',
|
||
ignore_unknown_columns => 1,
|
||
));
|
||
|
||
$csv_part_import->csv->parse;
|
||
|
||
$csv_part_import->controller->errors([ $csv_part_import->csv->errors ]) if $csv_part_import->csv->errors;
|
||
|
||
return if ( !$csv_part_import->csv->header || $csv_part_import->csv->errors );
|
||
|
||
my $headers = { headers => [ grep { $csv_part_import->csv->dispatcher->is_known($_, 0) } @{ $csv_part_import->csv->header } ] };
|
||
$headers->{methods} = [ map { $_->{path} } @{ $csv_part_import->csv->specs->[0] } ];
|
||
$headers->{used} = { map { ($_ => 1) } @{ $headers->{headers} } };
|
||
$csv_part_import->controller->headers($headers);
|
||
$csv_part_import->controller->raw_data_headers({ used => { }, headers => [ ] });
|
||
$csv_part_import->controller->info_headers({ used => { }, headers => [ ] });
|
||
|
||
my $objects = $csv_part_import->csv->get_objects;
|
||
my @raw_data = @{ $csv_part_import->csv->get_data };
|
||
|
||
$csv_part_import->controller->data([ pairwise { no warnings 'once'; { object => $a, raw_data => $b, errors => [], information => [], info_data => {} } } @$objects, @raw_data ]);
|
||
|
||
$csv_part_import->check_objects;
|
||
|
||
# don't try and save objects that have errors
|
||
$csv_part_import->save_objects unless scalar @{$csv_part_import->controller->data->[0]->{errors}};
|
||
|
||
return $csv_part_import->controller->data;
|
||
}
|
||
|
||
$::myconfig{numberformat} = '1000.00';
|
||
my $old_locale = $::locale;
|
||
# set locale to en so we can match errors
|
||
$::locale = Locale->new('en');
|
||
|
||
|
||
my ($entries, $entry, $file);
|
||
|
||
# different settings for tests
|
||
#
|
||
|
||
my $settings1 = {
|
||
sellprice_places => 2,
|
||
sellprice_adjustment => 0,
|
||
sellprice_adjustment_type => 'percent',
|
||
article_number_policy => 'update_prices',
|
||
shoparticle_if_missing => '0',
|
||
parts_type => 'part',
|
||
default_buchungsgruppe => ($bugru ? $bugru->id : undef),
|
||
apply_buchungsgruppe => 'all',
|
||
};
|
||
my $settings2 = {
|
||
sellprice_places => 2,
|
||
sellprice_adjustment => 0,
|
||
sellprice_adjustment_type => 'percent',
|
||
article_number_policy => 'update_parts',
|
||
shoparticle_if_missing => '0',
|
||
parts_type => 'part',
|
||
default_buchungsgruppe => ($bugru ? $bugru->id : undef),
|
||
apply_buchungsgruppe => 'missing',
|
||
default_unit => 'Stck',
|
||
};
|
||
|
||
#
|
||
#
|
||
# starting test of csv imports
|
||
# to debug errors in certain tests, run after test_import:
|
||
# die Dumper($entry->{errors});
|
||
|
||
|
||
##### create part
|
||
$file = \<<EOL;
|
||
partnumber;sellprice;lastcost;listprice;unit
|
||
P1000;100.10;90.20;95.30;kg
|
||
EOL
|
||
$entries = test_import($file,$settings1);
|
||
$entry = $entries->[0];
|
||
#foreach my $err ( @{ $entry->{errors} } ) {
|
||
# print $err;
|
||
#}
|
||
is $entry->{object}->partnumber,'P1000', 'partnumber';
|
||
is $entry->{object}->sellprice, '100.1', 'sellprice';
|
||
is $entry->{object}->lastcost, '90.2', 'lastcost';
|
||
is $entry->{object}->listprice, '95.3', 'listprice';
|
||
|
||
##### update prices of part
|
||
$file = \<<EOL;
|
||
partnumber;sellprice;lastcost;listprice;unit
|
||
P1000;110.10;95.20;97.30;kg
|
||
EOL
|
||
$entries = test_import($file,$settings1);
|
||
$entry = $entries->[0];
|
||
is $entry->{object}->sellprice, '110.1', 'updated sellprice';
|
||
is $entry->{object}->lastcost, '95.2', 'updated lastcost';
|
||
is $entry->{object}->listprice, '97.3', 'updated listprice';
|
||
|
||
##### insert parts with warehouse,bin name
|
||
|
||
$file = \<<EOL;
|
||
partnumber;description;warehouse;bin
|
||
P1000;Teil 1000;Lager1;Ort1_von_Lager1
|
||
P1001;Teil 1001;Lager1;Ort2_von_Lager1
|
||
P1002;Teil 1002;Lager2;Ort1_von_Lager2
|
||
P1003;Teil 1003;Lager2;Ort2_von_Lager2
|
||
EOL
|
||
$entries = test_import($file,$settings2);
|
||
$entry = $entries->[0];
|
||
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
|
||
is $entry->{object}->warehouse_id, $wh1->id, 'Lager1';
|
||
is $entry->{object}->bin_id, $bin1_1->id, 'Lagerort1';
|
||
$entry = $entries->[2];
|
||
is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
|
||
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
|
||
is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
|
||
|
||
##### update warehouse and bin
|
||
$file = \<<EOL;
|
||
partnumber;description;warehouse;bin
|
||
P1000;Teil 1000;Lager2;Ort1_von_Lager2
|
||
P1001;Teil 1001;Lager1;Ort1_von_Lager1
|
||
P1002;Teil 1002;Lager2;Ort1_von_Lager1
|
||
P1003;Teil 1003;Lager2;kein Lagerort
|
||
EOL
|
||
$entries = test_import($file,$settings2);
|
||
$entry = $entries->[0];
|
||
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
|
||
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
|
||
is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
|
||
$entry = $entries->[2];
|
||
my $err1 = @{ $entry->{errors} }[0];
|
||
#print "'".$err1."'\n";
|
||
is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
|
||
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
|
||
is $err1, 'Error: Bin Ort1_von_Lager1 is not from warehouse Lager2','kein Lager von Lager2';
|
||
$entry = $entries->[3];
|
||
$err1 = @{ $entry->{errors} }[0];
|
||
#print "'".$err1."'\n";
|
||
is $entry->{object}->description, 'Teil 1003', 'Teil 1003 set';
|
||
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
|
||
is $err1, 'Error: Invalid bin name kein Lagerort','kein Lagerort';
|
||
|
||
##### add translations
|
||
$file = \<<EOL;
|
||
partnumber;description;description_EN;notes_EN;description_IT;notes_IT
|
||
P1000;Teil 1000;descr EN 1000;notes EN;descr IT 1000;notes IT
|
||
P1001;Teil 1001;descr EN 1001;notes EN;descr IT 1001;notes IT
|
||
P1002;Teil 1002;descr EN 1002;notes EN;descr IT 1002;notes IT
|
||
P1003;Teil 1003;descr EN 1003;notes EN;descr IT 1003;notes IT
|
||
EOL
|
||
$entries = test_import($file,$settings2);
|
||
$entry = $entries->[0];
|
||
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
|
||
is $entry->{raw_data}->{description_EN},'descr EN 1000','EN set';
|
||
is $entry->{raw_data}->{description_IT},'descr IT 1000','IT set';
|
||
my $l = @{$entry->{object}->translations}[0];
|
||
is $l->translation,'descr EN 1000','EN trans set';
|
||
is $l->longdescription, 'notes EN','EN notes set';
|
||
$l = @{$entry->{object}->translations}[1];
|
||
is $l->translation,'descr IT 1000','IT trans set';
|
||
is $l->longdescription, 'notes IT','IT notes set';
|
||
|
||
##### add customvar
|
||
$file = \<<EOL;
|
||
partnumber;cvar_mycvar
|
||
P1000;das ist der ring
|
||
P1001;nicht der nibelungen
|
||
P1002;sondern vom
|
||
P1003;Herr der Ringe
|
||
EOL
|
||
$entries = test_import($file,$settings2);
|
||
$entry = $entries->[0];
|
||
is $entry->{object}->partnumber, 'P1000', 'P1000 set';
|
||
is $entry->{raw_data}->{cvar_mycvar},'das ist der ring','CVAR set';
|
||
is @{$entry->{object}->custom_variables}[0]->text_value,'das ist der ring','Cvar mit richtigem Weert';
|
||
|
||
clear_up(); # remove all data at end of tests
|
||
|
||
# end of tests
|
||
|
||
|
||
sub clear_up {
|
||
SL::DB::Manager::Part ->delete_all(all => 1);
|
||
SL::DB::Manager::Translation->delete_all(all => 1);
|
||
SL::DB::Manager::Language ->delete_all(all => 1);
|
||
SL::DB::Manager::Bin ->delete_all(all => 1);
|
||
SL::DB::Manager::Warehouse ->delete_all(all => 1);
|
||
SL::DB::Manager::CustomVariableConfig->delete_all(all => 1);
|
||
}
|
||
|
||
|
||
1;
|
||
|
||
#####
|
||
# vim: ft=perl
|
||
# set emacs to perl mode
|
||
# Local Variables:
|
||
# mode: perl
|
||
# End:
|
templates/webpages/csv_import/_form_parts.html | ||
---|---|---|
<tr>
|
||
<th align="right">[%- LxERP.t8('Parts with existing part numbers') %]:</th>
|
||
<td colspan="10">
|
||
[% opts = [ [ 'update_prices', LxERP.t8('Update prices of existing entries') ], [ 'insert_new', LxERP.t8('Insert with new part number') ], [ 'skip', LxERP.t8('Skip entry') ] ] %]
|
||
[% opts = [[ 'update_parts', LxERP.t8('Update properties of existing entries') ], [ 'update_parts_sn', LxERP.t8('Update properties of existing entries / skip non-existent') ], [ 'update_prices', LxERP.t8('Update prices of existing entries') ],[ 'update_prices_sn', LxERP.t8('Update prices of existing entries / skip non-existent') ] ,[ 'insert_new', LxERP.t8('Insert with new part number') ], [ 'skip', LxERP.t8('Skip entry') ] ] %]
|
||
[% L.select_tag('settings.article_number_policy', opts, default = SELF.profile.get('article_number_policy'), style = 'width: 300px') %]
|
||
</td>
|
||
</tr>
|
Auch abrufbar als: Unified diff
CSV-Import Artikel: Einige Erweiterungen
CSV-Import von Artikel hat nun für existierende Artikel folgende Optionen: