Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 52518527

Von Martin Helmling martin.helmling@octosoft.eu vor mehr als 8 Jahren hinzugefügt

  • ID 52518527bc507767386d21e1870cc2888269ba70
  • Vorgänger a68089fb
  • Nachfolger 07c884e5

CSV-Import Artikel: Einige Erweiterungen

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 weitere Bemerkungen an den einzelnen Importzeilen eingebaut.

Unterschiede anzeigen:

SL/Controller/CsvImport/Part.pm
24 24
(
25 25
 scalar                  => [ qw(table makemodel_columns) ],
26 26
 'scalar --get_set_init' => [ qw(bg_by settings parts_by price_factors_by units_by partsgroups_by
27
                                 warehouses_by bins_by
27 28
                                 translation_columns all_pricegroups) ],
28 29
);
29 30

  
......
78 79
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
79 80
}
80 81

  
82
sub init_bins_by {
83
  my ($self) = @_;
84

  
85
  my $all_bins = SL::DB::Manager::Bin->get_all;
86
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_bins } } ) } qw(id description) };
87
}
88

  
89
sub init_warehouses_by {
90
  my ($self) = @_;
91

  
92
  my $all_warehouses = SL::DB::Manager::Warehouse->get_all;
93
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
94
}
95

  
96

  
81 97
sub init_parts_by {
82 98
  my ($self) = @_;
83 99

  
......
145 161
    $self->check_price_factor($entry);
146 162
    $self->check_payment($entry);
147 163
    $self->check_partsgroup($entry);
164
    $self->check_warehouse_and_bin($entry);
148 165
    $self->handle_pricegroups($entry);
149 166
    $self->check_existing($entry) unless @{ $entry->{errors} };
150 167
    $self->handle_prices($entry) if $self->settings->{sellprice_adjustment};
......
156 173
  } continue {
157 174
    $i++;
158 175
  }
159

  
160 176
  $self->add_columns(qw(type)) if $self->settings->{parts_type} eq 'mixed';
161 177
  $self->add_columns(qw(buchungsgruppen_id unit));
162
  $self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup));
178
  $self->add_columns(map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw (price_factor payment partsgroup warehouse bin));
163 179
  $self->add_columns(qw(shop)) if $self->settings->{shoparticle_if_missing};
164 180
  $self->add_cvar_raw_data_columns;
165 181
  map { $self->add_raw_data_columns("pricegroup_${_}") if exists $self->controller->data->[0]->{raw_data}->{"pricegroup_$_"} } (1..scalar(@{ $self->all_pricegroups }));
......
198 214
  $default_id    = undef unless $self->bg_by->{id}->{ $default_id };
199 215

  
200 216
  # 1. Use default ID if enforced.
201
  $object->buchungsgruppen_id($default_id) if $default_id && ($self->settings->{apply_buchungsgruppe} eq 'all');
217
  if ($default_id && ($self->settings->{apply_buchungsgruppe} eq 'all')) {
218
    $object->buchungsgruppen_id($default_id);
219
    push @{ $entry->{information} }, $::locale->text('Use default booking group because setting is \'all\'');
220
  }
202 221

  
203 222
  # 2. Use supplied ID if valid
204 223
  $object->buchungsgruppen_id(undef) if $object->buchungsgruppen_id && !$self->bg_by->{id}->{ $object->buchungsgruppen_id };
......
210 229
  }
211 230

  
212 231
  # 4. Use default ID if not valid.
213
  $object->buchungsgruppen_id($default_id) if !$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing');
214

  
232
  if (!$object->buchungsgruppen_id && $default_id && ($self->settings->{apply_buchungsgruppe} eq 'missing')) {
233
    $object->buchungsgruppen_id($default_id) ;
234
    $entry->{buch_information} = $::locale->text('Use default booking group because wanted is missing');
235
  }
215 236
  return 1 if $object->buchungsgruppen_id;
237
  $entry->{buch_error} =  $::locale->text('Error: booking group missing or invalid');
238
  return 0;
239
}
216 240

  
217
  push @{ $entry->{errors} }, $::locale->text('Error: booking group missing or invalid');
241
sub _part_is_used {
242
  my ($self, $part) = @_;
243

  
244
  my $query =
245
      qq|SELECT COUNT(parts_id) FROM invoice where parts_id = ?
246
         UNION
247
         SELECT COUNT(parts_id) FROM assembly where parts_id = ?
248
         UNION
249
         SELECT COUNT(parts_id) FROM orderitems where parts_id = ?
250
        |;
251
  foreach my $ref (selectall_hashref_query($::form, $part->db->dbh, $query, $part->id, $part->id, $part->id)) {
252
    return 1 if $ref->{count} != 0;
253
  }
218 254
  return 0;
219 255
}
220 256

  
......
222 258
  my ($self, $entry) = @_;
223 259

  
224 260
  my $object = $entry->{object};
261
  my $raw = $entry->{raw_data};
225 262

  
226 263
  if ($object->partnumber && $self->parts_by->{partnumber}{$object->partnumber}) {
227
    $entry->{part} = SL::DB::Manager::Part->find_by(partnumber => $object->partnumber);
264
    $entry->{part} = SL::DB::Manager::Part->get_all( query => [ partnumber => $object->partnumber ], limit => 1,
265
      with_objects => [ 'translations', 'custom_variables' ]
266
    ) -> [0];
267
    if ( !$entry->{part} ) {
268
        $entry->{part} = SL::DB::Manager::Part->get_all( query => [ partnumber => $object->partnumber ], limit => 1,
269
          with_objects => [ 'translations' ]
270
        ) -> [0];
271
    }
228 272
  }
229 273

  
230 274
  if ($entry->{part}) {
231
    if ($self->settings->{article_number_policy} eq 'update_prices') {
232
      if ($self->settings->{parts_type} eq 'mixed' && $entry->{part}->type ne $object->type) {
233
        push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
234
      } else {
235
        map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
275
    if ($entry->{part}->type ne $object->type ) {
276
      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database with different type'));
277
      return;
278
    }
279
    if ( $entry->{part}->unit != $object->unit || $entry->{part}->inventory_accno_id != $object->inventory_accno_id ) {
280
      if ( $entry->{part}->onhand != 0 || $self->_part_is_used($entry->{part})) {
281
        push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry with different unit or inventory_accno_id'));
282
        return;
283
      }
284
    }
285
  }
236 286

  
287
  if ($self->settings->{article_number_policy} eq 'update_prices_sn' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
288
    if (!$entry->{part}) {
289
      push(@{$entry->{errors}}, $::locale->text('Skipping non-existent article'));
290
      return;
291
    }
292
  }
293

  
294
  ## checking also doubles in csv !!
295
  foreach my $csventry (@{ $self->controller->data }) {
296
    if ( $entry != $csventry && $object->partnumber eq $csventry->{object}->partnumber ) {
297
      if ( $csventry->{doublechecked} ) {
298
        push(@{$entry->{errors}}, $::locale->text('Skipping due to same partnumber in csv file'));
299
        return;
300
      }
301
    }
302
  }
303
  $entry->{doublechecked} = 1;
304

  
305
  if ($entry->{part}) {
306
    if ($self->settings->{article_number_policy} eq 'update_prices' || $self->settings->{article_number_policy} eq 'update_prices_sn') {
307
      map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ } qw(sellprice listprice lastcost);
308

  
309
      # merge prices
310
      my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
311
      $entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
312

  
313
      push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
314
      $entry->{object_to_save} = $entry->{part};
315
    } elsif ( $self->settings->{article_number_policy} eq 'update_parts' || $self->settings->{article_number_policy} eq 'update_parts_sn') {
316

  
317
      # Update parts table
318
      # copy only the data which is not explicit copied by  "methods"
319

  
320
      map { $entry->{part}->$_( $object->$_ ) if defined $object->$_ }  qw(description notes weight ean rop image
321
                                                                           drawing ve gv
322
                                                                           unit
323
                                                                           has_sernumber not_discountable obsolete
324
                                                                           payment_id
325
                                                                           sellprice listprice lastcost);
326

  
327
      if (defined $raw->{"sellprice"} || defined $raw->{"listprice"} || defined $raw->{"lastcost"}) {
237 328
        # merge prices
238 329
        my %prices_by_pricegroup_id = map { $_->pricegroup->id => $_ } $entry->{part}->prices, $object->prices;
239 330
        $entry->{part}->prices(grep { $_ } map { $prices_by_pricegroup_id{$_->id} } @{ $self->all_pricegroups });
331
      }
240 332

  
241
        push @{ $entry->{information} }, $::locale->text('Updating prices of existing entry in database');
242
        $entry->{object_to_save} = $entry->{part};
333
      # Update translation
334
      my @translations;
335
      push @translations, $entry->{part}->translations;
336
      foreach my $language (@{ $self->all_languages }) {
337
        my $desc;
338
        $desc = $raw->{"description_". $language->article_code}  if defined $raw->{"description_". $language->article_code};
339
        my $notes;
340
        $notes = $raw->{"notes_". $language->article_code}  if defined $raw->{"notes_". $language->article_code};
341
        next unless $desc || $notes;
342

  
343
        push @translations, SL::DB::Translation->new(language_id     => $language->id,
344
                                                     translation     => $desc,
345
                                                     longdescription => $notes);
243 346
      }
244
    } elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
245
      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database'));
347
      $entry->{part}->translations(\@translations) if @translations;
348

  
349
      # Update cvars
350
      my %type_to_column = ( text      => 'text_value',
351
                             textfield => 'text_value',
352
                             select    => 'text_value',
353
                             date      => 'timestamp_value_as_date',
354
                             timestamp => 'timestamp_value_as_date',
355
                             number    => 'number_value_as_number',
356
                             bool      => 'bool_value' );
357
      my @cvars;
358
      push @cvars, $entry->{part}->custom_variables;
359
      foreach my $config (@{ $self->all_cvar_configs }) {
360
        next unless exists $raw->{ "cvar_" . $config->name };
361
        my $value  = $raw->{ "cvar_" . $config->name };
362
        my $column = $type_to_column{ $config->type } || die "Program logic error: unknown custom variable storage type";
363
        push @cvars, SL::DB::CustomVariable->new(config_id => $config->id, $column => $value, sub_module => '');
364
      }
365
      $entry->{part}->custom_variables(\@cvars) if @cvars;
366

  
367
      # save Part Update
368
      push @{ $entry->{information} }, $::locale->text('Updating data of existing entry in database');
369

  
370
      $entry->{object_to_save} = $entry->{part};
371
      # copy all other data via "methods"
372
      my $methods        = $self->controller->headers->{methods};
373
      $entry->{object_to_save}->$_( $entry->{object}->$_ ) for @{ $methods }, keys %{ $self->clone_methods };
246 374

  
375
    } elsif ( $self->settings->{article_number_policy} eq 'skip' ) {
376
      push(@{$entry->{errors}}, $::locale->text('Skipping due to existing entry in database')) if ( $entry->{part} );
247 377
    } else {
248
      $object->partnumber('####');
249
      push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
378
      #$object->partnumber('####');
250 379
    }
251 380
  } else {
252
    push(@{$entry->{errors}}, $::locale->text('Skipping, for assemblies are not importable (yet)')) if $object->type eq 'assembly';
381
    # set error or info from buch if part not exists
382
    push @{ $entry->{information} }, $entry->{buch_information} if $entry->{buch_information};
383
    push @{ $entry->{errors} }, $entry->{buch_error} if $entry->{buch_error};
253 384
  }
254 385
}
255 386

  
......
275 406
sub check_type {
276 407
  my ($self, $entry) = @_;
277 408

  
278
  my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
279
  $bg  ||= SL::DB::Buchungsgruppe->new(inventory_accno_id => 1); # does this case ever occur?
280

  
281 409
  my $type = $self->settings->{parts_type};
282
  if ($type eq 'mixed') {
410

  
411
  if ($type eq 'mixed' && $entry->{raw_data}->{type}) {
283 412
    $type = $entry->{raw_data}->{type} =~ m/^p/i ? 'part'
284 413
          : $entry->{raw_data}->{type} =~ m/^s/i ? 'service'
285 414
          : $entry->{raw_data}->{type} =~ m/^a/i ? 'assembly'
286 415
          :                                        undef;
287 416
  }
288 417

  
289
  $entry->{object}->assembly($type eq 'assembly');
290

  
291 418
  # when saving income_accno_id or expense_accno_id use ids from the selected
292 419
  # $bg according to the default tax_zone (the one with the highest sort
293 420
  # order).  Alternatively one could use the ids from defaults, but they might
294 421
  # not all be set.
422
  # Only use existing bg
295 423

  
296
  $entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
424
  my $bg = $self->bg_by->{id}->{ $entry->{object}->buchungsgruppen_id };
297 425

  
298
  if ($type eq 'part' || $type eq 'service') {
299
    $entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
300
  }
426
  # if not set there is an error occurred in check_buchungsgruppe()
427
  # but if the part exists the new values for accno are ignored
301 428

  
302
  if ($type eq 'part') {
303
    $entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
304
  }
429
  if ( $bg ) {
430
    $entry->{object}->income_accno_id( $bg->income_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
431
    $self->clone_methods->{income_accno_id} = 1;
305 432

  
306
  if (none { $_ eq $type } qw(part service assembly)) {
307
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid part type');
308
    return 0;
433
    if ($type eq 'part' || $type eq 'service') {
434
      $entry->{object}->expense_accno_id( $bg->expense_accno_id( SL::DB::Manager::TaxZone->get_default->id ) );
435
      $self->clone_methods->{expense_accno_id} = 1;
436
    }
309 437
  }
310 438

  
439
  if ($type eq 'part') {
440
    if ( $bg ) {
441
      $entry->{object}->inventory_accno_id( $bg->inventory_accno_id );
442
    }
443
    else {
444
      #use an existent bg
445
      $entry->{object}->inventory_accno_id( SL::DB::Manager::Buchungsgruppe->get_first->id );
446
    }
447
  } elsif ($type eq 'assembly') {
448
      $entry->{object}->assembly(1);
449
  }
311 450
  return 1;
312 451
}
313 452

  
......
332 471
    }
333 472

  
334 473
    $object->price_factor_id($pf->id);
474
    $self->clone_methods->{price_factor_id} = 1;
475
  }
476

  
477
  return 1;
478
}
479

  
480
sub check_warehouse_and_bin {
481
  my ($self, $entry) = @_;
482

  
483
  my $object = $entry->{object};
484

  
485
  # Check whether or not warehouse id is valid.
486
  if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
487
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse id');
488
    return 0;
489
  }
490
  # Map name to ID if given.
491
  if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
492
    my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
493

  
494
    if (!$wh) {
495
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse name #1',$entry->{raw_data}->{warehouse});
496
      return 0;
497
    }
498

  
499
    $object->warehouse_id($wh->id);
500
  }
501
  $self->clone_methods->{warehouse_id} = 1;
502

  
503
  # Check whether or not bin id is valid.
504
  if ($object->bin_id && !$self->bins_by->{id}->{ $object->bin_id }) {
505
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin id');
506
    return 0;
335 507
  }
508
  # Map name to ID if given.
509
  if ($object->warehouse_id && !$object->bin_id && $entry->{raw_data}->{bin}) {
510
    my $bin = $self->bins_by->{description}->{ $entry->{raw_data}->{bin} };
511

  
512
    if (!$bin) {
513
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin name #1',$entry->{raw_data}->{bin});
514
      return 0;
515
    }
336 516

  
517
    $object->bin_id($bin->id);
518
  }
519
  $self->clone_methods->{bin_id} = 1;
520

  
521
  if ($object->warehouse_id && $object->bin_id ) {
522
    my $bin = $self->bins_by->{id}->{ $object->bin_id };
523
    if ( $bin->warehouse_id != $object->warehouse_id ) {
524
      push @{ $entry->{errors} }, $::locale->text('Error: Bin #1 is not from warehouse #2',
525
                                                  $self->bins_by->{id}->{$object->bin_id}->description,
526
                                                  $self->warehouses_by->{id}->{ $object->warehouse_id }->description);
527
      return 0;
528
    }
529
  }
337 530
  return 1;
338 531
}
339 532

  
......
359 552

  
360 553
    $object->partsgroup_id($pg->id);
361 554
  }
555
  # register payment_id for method copying later
556
  $self->clone_methods->{partsgroup_id} = 1;
362 557

  
363 558
  return 1;
364 559
}
......
480 675
  my ($self) = @_;
481 676

  
482 677
  my $profile = $self->SUPER::init_profile;
483
  delete @{$profile}{qw(assembly bom expense_accno_id income_accno_id inventory_accno_id makemodel priceupdate stockable type)};
678
  delete @{$profile}{qw(alternate assembly bom expense_accno_id income_accno_id inventory_accno_id makemodel priceupdate stockable type)};
484 679

  
485 680
  $profile->{"pricegroup_$_"} = '' for 1 .. scalar @{ $_[0]->all_pricegroups };
486 681

  
......
505 700
  $self->SUPER::setup_displayable_columns;
506 701
  $self->add_cvar_columns_to_displayable_columns;
507 702

  
508
  $self->add_displayable_columns({ name => 'bin',                description => $::locale->text('Bin')                                                  },
509
                                 { name => 'buchungsgruppen_id', description => $::locale->text('Booking group (database ID)')                          },
510
                                 { name => 'buchungsgruppe',     description => $::locale->text('Booking group (name)')                                 },
703
  $self->add_displayable_columns({ name => 'assembly',           description => $::locale->text('assembly')                                             },
704
                                 { name => 'bin_id',             description => $::locale->text('Bin (database ID)')                                    },
705
                                 { name => 'bin',                description => $::locale->text('Bin (name)')                                           },
706
                                 { name => 'buchungsgruppen_id', description => $::locale->text('Booking group (database ID)')                         },
707
                                 { name => 'buchungsgruppe',     description => $::locale->text('Booking group (name)')                                },
511 708
                                 { name => 'description',        description => $::locale->text('Description')                                          },
512 709
                                 { name => 'drawing',            description => $::locale->text('Drawing')                                              },
513 710
                                 { name => 'ean',                description => $::locale->text('EAN')                                                  },
......
515 712
                                 { name => 'gv',                 description => $::locale->text('Business Volume')                                      },
516 713
                                 { name => 'has_sernumber',      description => $::locale->text('Has serial number')                                    },
517 714
                                 { name => 'image',              description => $::locale->text('Image')                                                },
715
                                 { name => 'inventory_accno_id', description => $::locale->text('part')                                                 },
518 716
                                 { name => 'lastcost',           description => $::locale->text('Last Cost')                                            },
519 717
                                 { name => 'listprice',          description => $::locale->text('List Price')                                           },
520 718
                                 { name => 'make_X',             description => $::locale->text('Make (vendor\'s database ID, number or name; with X being a number)') . ' [1]' },
......
534 732
                                 { name => 'price_factor',       description => $::locale->text('Price factor (name)')                                  },
535 733
                                 { name => 'rop',                description => $::locale->text('ROP')                                                  },
536 734
                                 { name => 'sellprice',          description => $::locale->text('Sellprice')                                            },
537
                                 { name => 'shop',               description => $::locale->text('Shop article')                                         },
735
                                 { name => 'shop',               description => $::locale->text('Shop article')                                          },
538 736
                                 { name => 'type',               description => $::locale->text('Article type')  . ' [3]'                             },
539 737
                                 { name => 'unit',               description => $::locale->text('Unit (if missing or empty default unit will be used)') },
540 738
                                 { name => 've',                 description => $::locale->text('Verrechnungseinheit')                                  },
739
                                 { name => 'warehouse_id',       description => $::locale->text('Warehouse (database ID)')                              },
740
                                 { name => 'warehouse',          description => $::locale->text('Warehouse (name)')                              },
541 741
                                 { name => 'weight',             description => $::locale->text('Weight')                                               },
542 742
                                );
543 743

  
doc/changelog
8 8

  
9 9
  - Für UStVA Voranmeldung über Elster gibt es die Anbindung über Geierlein (Installation/Config siehe Commit)
10 10
  
11
  - CSV-Import von Artikel hat nun für existierende Artikel folgende Optionen:
12
     1. Eigenschaften von existierenden Einträgen aktualisieren
13
     2. Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen
14
     3. Preise von vorhandenen Artikeln aktualisieren
15
     4. Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen
16
     5. Mit neuer Artikelnummer einfügen
17
     6. Eintrag überspringen
18
    Zusätzlich können nun Spalten "Lager","Lagerort" als Name oder ID eingelesen werden,
19
    sowie Übersetzungen z.B. als 'description_EN' oder 'description_IT'.
20
    Auch cvars können als 'cvars_<name>' importiert werden.
21
    Ebenfalls sind zusätzliche Bemerkungen an den einzelnen Importzeilen eingebaut.
22
 
11 23
  - In der Lager-Mandantenkonfig gibt es das Feature "Zum Fertigen Standardlager des Bestandteils verwenden".
12 24
    Statt das Ziellager des Erzeugnisses zu Verwenden, wird nun zur Prüfung der Fertigung das
13 25
    Standardlager der einzelnen Bestandteile verwendet.
locale/de/all
408 408
  'Billing/shipping address (zipcode)' => 'Rechnungs-/Lieferadresse (PLZ)',
409 409
  'Bin'                         => 'Lagerplatz',
410 410
  'Bin (database ID)'           => 'Lagerplatz (Datenbank-ID)',
411
  'Bin (name)'                  => 'Lagerplatz (Name)',
411 412
  'Bin From'                    => 'Quelllagerplatz',
412 413
  'Bin List'                    => 'Lagerliste',
413 414
  'Bin To'                      => 'Ziellagerplatz',
......
427 428
  'Booking group #1 needs a valid expense account' => 'Buchungsgruppe #1 braucht ein gültiges Aufwandskonto',
428 429
  'Booking group #1 needs a valid income account' => 'Buchungsgruppe #1 braucht ein gültiges Erfolgskonto',
429 430
  'Booking group #1 needs a valid inventory account' => 'Buchungsgruppe #1 braucht ein gültiges Warenbestandskonto',
430
  'Booking group (database ID)' => 'Buchungsgruppe (Datenbank-ID)',
431
  'Booking group (name)'        => 'Buchungsgruppe (Name)',
431
  'Booking group (database ID)' => 'Buchungsgruppe (database ID)',
432
  'Booking group (name)'        => 'Buchungsgruppe (name)',
432 433
  'Booking groups'              => 'Buchungsgruppen',
433 434
  'Books are open'              => 'Die Bücher sind geöffnet.',
434 435
  'Books closed up to'          => 'Bücher abgeschlossen bis zum',
......
1151 1152
  'Error: A negative target quantity is not allowed.' => 'Fehler: Eine negative Zielmenge ist nicht erlaubt.',
1152 1153
  'Error: A quantity and a target quantity could not be given both.' => 'Fehler: Menge und Zielmenge können nicht beide angegeben werden.',
1153 1154
  'Error: A quantity or a target quantity must be given.' => 'Fehler: Menge oder Zielmenge muss angegeben werden.',
1155
  'Error: Bin #1 is not from warehouse #2' => 'Lager \'#2\' hat keinen Lagerplatz \'#1\'',
1154 1156
  'Error: Bin not found'        => 'Fehler: Lagerplatz nicht gefunden',
1155 1157
  'Error: Customer/vendor missing' => 'Fehler: Kunde/Lieferant fehlt',
1156 1158
  'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
1157 1159
  'Error: Found local bank account number but local bank code doesn\'t match' => 'Fehler: Kontonummer wurde gefunden aber gespeicherte Bankleitzahl stimmt nicht überein',
1158 1160
  'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
1159 1161
  'Error: Invalid bin'          => 'Fehler: Ungültiger Lagerplatz',
1162
  'Error: Invalid bin id'       => 'Ungültige Lagerplatz-ID',
1163
  'Error: Invalid bin name #1'  => 'Ungültiger Lagerplatz \'#1\'',
1160 1164
  'Error: Invalid business'     => 'Fehler: Kunden-/Lieferantentyp ungültig',
1161 1165
  'Error: Invalid contact'      => 'Fehler: Ansprechperson ungültig',
1162 1166
  'Error: Invalid currency'     => 'Fehler: ungültige Währung',
......
1165 1169
  'Error: Invalid language'     => 'Fehler: Sprache ungültig',
1166 1170
  'Error: Invalid order for this order item' => 'Fehler: Auftrag für diese Position ungültig',
1167 1171
  'Error: Invalid part'         => 'Fehler: Artikel ungültig',
1168
  'Error: Invalid part type'    => 'Fehler: Artikeltyp ungültig',
1169 1172
  'Error: Invalid parts group'  => 'Fehler: Warengruppe ungültig',
1170 1173
  'Error: Invalid payment terms' => 'Fehler: Zahlungsbedingungen ungültig',
1171 1174
  'Error: Invalid price factor' => 'Fehler: Preisfaktor ungültig',
......
1177 1180
  'Error: Invalid unit'         => 'Fehler: Einheit ungültig',
1178 1181
  'Error: Invalid vendor in column make_#1' => 'Fehler: Lieferant ungültig in Spalte make_#1',
1179 1182
  'Error: Invalid warehouse'    => 'Fehler: Ungültiges Lager',
1183
  'Error: Invalid warehouse id' => 'Ungültige Lager-ID',
1184
  'Error: Invalid warehouse name #1' => 'Ungültiger Lagername \'#1\'',
1180 1185
  'Error: Name missing'         => 'Fehler: Name fehlt',
1181 1186
  'Error: Part not found'       => 'Fehler: Artikel nicht gefunden',
1182 1187
  'Error: Quantity to transfer is zero.' => 'Fehler: Zu bewegende Menge ist Null.',
......
2578 2583
  'Skipping due to existing bank transaction in database' => 'Wegen schon existierender Bankbewegung in Datenbank übersprungen',
2579 2584
  'Skipping due to existing entry in database' => 'Wegen existierendem Eintrag mit selber Nummer übersprungen',
2580 2585
  'Skipping due to existing entry in database with different type' => 'Wegen existierendem Eintrag von unterschiedlichem Artikeltyp übersprungen',
2581
  'Skipping, for assemblies are not importable (yet)' => 'Übersprungen, da Erzeugnisse (noch) nicht importiert werden können',
2586
  'Skipping due to existing entry with different unit or inventory_accno_id' => 'Wegen existierendem und verwendetem Eintrag von unterschiedlicher Einheit oder Buchungsgruppe übersprungen',
2587
  'Skipping due to same partnumber in csv file' => 'Eintrag in Datei mit doppelter Artikelnummer wird übersprungen',
2588
  'Skipping non-existent article' => 'Überspringe nicht vorhandenen Artikel',
2582 2589
  'Skonto'                      => 'Skonto',
2583 2590
  'Skonto Terms'                => 'Zahlungsziel Skonto',
2584 2591
  'Skonto amount'               => 'Skontobetrag',
......
3241 3248
  'Update SKR04: new tax account 3804 (19%)' => 'Update SKR04: neues Steuerkonto 3804 (19%) für innergemeinschaftlichen Erwerb',
3242 3249
  'Update prices'               => 'Preise aktualisieren',
3243 3250
  'Update prices of existing entries' => 'Preise von vorhandenen Artikeln aktualisieren',
3251
  'Update prices of existing entries / skip non-existent' => 'Preise von vorhandenen Artikel aktualisieren / Nicht vorhandene überspringen',
3244 3252
  'Update properties of existing entries' => 'Eigenschaften von existierenden Einträgen aktualisieren',
3253
  'Update properties of existing entries / skip non-existent' => 'Eigenschaften von existierenden Artikeln aktualisieren / Nicht vorhandene überspringen',
3245 3254
  'Update quotation/order'      => 'Auftrag/Angebot aktualisieren',
3246 3255
  'Update sales order #1'       => 'Kundenauftrag #1 aktualisieren',
3247 3256
  'Update sales quotation #1'   => 'Angebot #1 aktualisieren',
3248 3257
  'Update this draft.'          => 'Aktuellen Entwurf speichern',
3249 3258
  'Update with section'         => 'Mit Abschnitt aktualisieren',
3250 3259
  'Updated'                     => 'Erneuert am',
3260
  'Updating data of existing entry in database' => 'Aktualisierung von vorhandenen Datenbankdaten',
3251 3261
  'Updating existing entry in database' => 'Existierenden Eintrag in Datenbank aktualisieren',
3252 3262
  'Updating items with additional parts' => 'Positionen für zusätzliche Artikel aktualisieren',
3253 3263
  'Updating items with sections' => 'Positionen für Abschnitte aktualisieren',
......
3262 3272
  'Use Income'                  => 'GUV und BWA verwenden',
3263 3273
  'Use UStVA'                   => 'UStVA verwenden',
3264 3274
  'Use WebDAV Repository'       => 'WebDAV-Ablage verwenden',
3275
  'Use default booking group because setting is \'all\'' => 'Standardbuchungsgruppe wird verwendet',
3276
  'Use default booking group because wanted is missing' => 'Fehlende Buchungsgruppe, deshalb Standardbuchungsgruppe',
3265 3277
  'Use default warehouse for assembly transfer' => 'Zum Fertigen Standardlager des Bestandteils verwenden',
3266 3278
  'Use existing templates'      => 'Vorhandene Druckvorlagen verwenden',
3267 3279
  'Use linked items'            => 'Verknüpfte Positionen verwenden',
......
3329 3341
  'WHJournal'                   => 'Lagerbuchungen',
3330 3342
  'Warehouse'                   => 'Lager',
3331 3343
  'Warehouse (database ID)'     => 'Lager (Datenbank-ID)',
3344
  'Warehouse (name)'            => 'Lager (Name)',
3332 3345
  'Warehouse From'              => 'Quelllager',
3333 3346
  'Warehouse Migration'         => 'Lagermigration',
3334 3347
  'Warehouse To'                => 'Ziellager',
t/controllers/csvimport/parts.t
1
use Test::More tests => 33;
2

  
3
use strict;
4

  
5
use lib 't';
6

  
7
use Carp;
8
use Data::Dumper;
9
use Support::TestSetup;
10
use Test::Exception;
11

  
12
use List::MoreUtils qw(pairwise);
13
use SL::Controller::CsvImport;
14

  
15
my $DEBUG = 0;
16

  
17
use_ok 'SL::Controller::CsvImport::Part';
18

  
19
use SL::DB::Buchungsgruppe;
20
use SL::DB::Currency;
21
use SL::DB::Customer;
22
use SL::DB::Language;
23
use SL::DB::Warehouse;
24
use SL::DB::Bin;
25

  
26
my ($translation, $bin1_1, $bin1_2, $bin2_1, $bin2_2, $wh1, $wh2, $bugru, $cvarconfig );
27

  
28
Support::TestSetup::login();
29

  
30
sub reset_state {
31
  # Create test data
32

  
33
  clear_up();
34

  
35
  $translation     = SL::DB::Language->new(
36
    description    => 'Englisch',
37
    article_code   => 'EN',
38
    template_code  => 'EN',
39
  )->save;
40
  $translation     = SL::DB::Language->new(
41
    description    => 'Italienisch',
42
    article_code   => 'IT',
43
    template_code  => 'IT',
44
  )->save;
45
  $wh1 = SL::DB::Warehouse->new(
46
    description    => 'Lager1',
47
    sortkey        => 1,
48
  )->save;
49
  $bin1_1 = SL::DB::Bin->new(
50
    description    => 'Ort1_von_Lager1',
51
    warehouse_id   => $wh1->id,
52
  )->save;
53
  $bin1_2 = SL::DB::Bin->new(
54
    description    => 'Ort2_von_Lager1',
55
    warehouse_id   => $wh1->id,
56
  )->save;
57
  $wh2 = SL::DB::Warehouse->new(
58
    description    => 'Lager2',
59
    sortkey        => 2,
60
  )->save;
61
  $bin2_1 = SL::DB::Bin->new(
62
    description    => 'Ort1_von_Lager2',
63
    warehouse_id   => $wh2->id,
64
  )->save;
65
  $bin2_2 = SL::DB::Bin->new(
66
    description    => 'Ort2_von_Lager2',
67
    warehouse_id   => $wh2->id,
68
  )->save;
69

  
70
  $cvarconfig = SL::DB::CustomVariableConfig->new(
71
    module   => 'IC',
72
    name     => 'mycvar',
73
    type     => 'text',
74
    description => 'mein schattz',
75
    searchable  => 1,
76
    sortkey => 1,
77
    includeable => 0,
78
    included_by_default => 0,
79
  )->save;
80
}
81

  
82
$bugru = SL::DB::Manager::Buchungsgruppe->find_by(description => { like => 'Standard%19%' });
83

  
84
reset_state();
85

  
86
#####
87
sub test_import {
88
  my ($file,$settings) = @_;
89
  my @profiles;
90
  my $controller = SL::Controller::CsvImport->new();
91

  
92
  my $csv_part_import = SL::Controller::CsvImport::Part->new(
93
    settings   => $settings,
94
    controller => $controller,
95
    file       => $file,
96
  );
97

  
98
  $csv_part_import->init_bg_by;
99
  $csv_part_import->init_price_factors_by;
100
  $csv_part_import->init_partsgroups_by;
101
  $csv_part_import->init_units_by;
102
  $csv_part_import->init_bins_by;
103
  $csv_part_import->init_warehouses_by;
104
  $csv_part_import->init_parts_by;
105
  $csv_part_import->test_run(0);
106
  $csv_part_import->csv(SL::Helper::Csv->new(file                    => $csv_part_import->file,
107
                                             profile                 => [{ profile => $csv_part_import->profile,
108
                                                                           class   => $csv_part_import->class,
109
                                                                           mapping => $csv_part_import->controller->mappings_for_profile }],
110
                                             encoding                => 'utf-8',
111
                                             ignore_unknown_columns  => 1,
112
                                             strict_profile          => 1,
113
                                             case_insensitive_header => 1,
114
                                             sep_char                => ';',
115
                                             quote_char              => '"',
116
                                             ignore_unknown_columns  => 1,
117
                                            ));
118

  
119
  $csv_part_import->csv->parse;
120

  
121
  $csv_part_import->controller->errors([ $csv_part_import->csv->errors ]) if $csv_part_import->csv->errors;
122

  
123
  return if ( !$csv_part_import->csv->header || $csv_part_import->csv->errors );
124

  
125
  my $headers         = { headers => [ grep { $csv_part_import->csv->dispatcher->is_known($_, 0) } @{ $csv_part_import->csv->header } ] };
126
  $headers->{methods} = [ map { $_->{path} } @{ $csv_part_import->csv->specs->[0] } ];
127
  $headers->{used}    = { map { ($_ => 1) }  @{ $headers->{headers} } };
128
  $csv_part_import->controller->headers($headers);
129
  $csv_part_import->controller->raw_data_headers({ used => { }, headers => [ ] });
130
  $csv_part_import->controller->info_headers({ used => { }, headers => [ ] });
131

  
132
  my $objects  = $csv_part_import->csv->get_objects;
133
  my @raw_data = @{ $csv_part_import->csv->get_data };
134

  
135
  $csv_part_import->controller->data([ pairwise { no warnings 'once'; { object => $a, raw_data => $b, errors => [], information => [], info_data => {} } } @$objects, @raw_data ]);
136

  
137
  $csv_part_import->check_objects;
138

  
139
  # don't try and save objects that have errors
140
  $csv_part_import->save_objects unless scalar @{$csv_part_import->controller->data->[0]->{errors}};
141

  
142
  return $csv_part_import->controller->data;
143
}
144

  
145
$::myconfig{numberformat} = '1000.00';
146
my $old_locale = $::locale;
147
# set locale to en so we can match errors
148
$::locale = Locale->new('en');
149

  
150

  
151
my ($entries, $entry, $file);
152

  
153
# different settings for tests
154
#
155

  
156
my $settings1 = {
157
                       sellprice_places          => 2,
158
                       sellprice_adjustment      => 0,
159
                       sellprice_adjustment_type => 'percent',
160
                       article_number_policy     => 'update_prices',
161
                       shoparticle_if_missing    => '0',
162
                       parts_type                => 'part',
163
                       default_buchungsgruppe    => ($bugru ? $bugru->id : undef),
164
                       apply_buchungsgruppe      => 'all',
165
                };
166
my $settings2 = {
167
                       sellprice_places          => 2,
168
                       sellprice_adjustment      => 0,
169
                       sellprice_adjustment_type => 'percent',
170
                       article_number_policy     => 'update_parts',
171
                       shoparticle_if_missing    => '0',
172
                       parts_type                => 'part',
173
                       default_buchungsgruppe    => ($bugru ? $bugru->id : undef),
174
                       apply_buchungsgruppe      => 'missing',
175
                       default_unit              => 'Stck',
176
                };
177

  
178
#
179
#
180
# starting test of csv imports
181
# to debug errors in certain tests, run after test_import:
182
#   die Dumper($entry->{errors});
183

  
184

  
185
##### create part
186
$file = \<<EOL;
187
partnumber;sellprice;lastcost;listprice;unit
188
P1000;100.10;90.20;95.30;kg
189
EOL
190
$entries = test_import($file,$settings1);
191
$entry = $entries->[0];
192
#foreach my $err ( @{ $entry->{errors} } ) {
193
#  print $err;
194
#}
195
is $entry->{object}->partnumber,'P1000', 'partnumber';
196
is $entry->{object}->sellprice, '100.1', 'sellprice';
197
is $entry->{object}->lastcost,   '90.2', 'lastcost';
198
is $entry->{object}->listprice,  '95.3', 'listprice';
199

  
200
##### update prices of part
201
$file = \<<EOL;
202
partnumber;sellprice;lastcost;listprice;unit
203
P1000;110.10;95.20;97.30;kg
204
EOL
205
$entries = test_import($file,$settings1);
206
$entry = $entries->[0];
207
is $entry->{object}->sellprice, '110.1', 'updated sellprice';
208
is $entry->{object}->lastcost,   '95.2', 'updated lastcost';
209
is $entry->{object}->listprice,  '97.3', 'updated listprice';
210

  
211
##### insert parts with warehouse,bin name
212

  
213
$file = \<<EOL;
214
partnumber;description;warehouse;bin
215
P1000;Teil 1000;Lager1;Ort1_von_Lager1
216
P1001;Teil 1001;Lager1;Ort2_von_Lager1
217
P1002;Teil 1002;Lager2;Ort1_von_Lager2
218
P1003;Teil 1003;Lager2;Ort2_von_Lager2
219
EOL
220
$entries = test_import($file,$settings2);
221
$entry = $entries->[0];
222
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
223
is $entry->{object}->warehouse_id, $wh1->id, 'Lager1';
224
is $entry->{object}->bin_id, $bin1_1->id, 'Lagerort1';
225
$entry = $entries->[2];
226
is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
227
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
228
is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
229

  
230
##### update warehouse and bin
231
$file = \<<EOL;
232
partnumber;description;warehouse;bin
233
P1000;Teil 1000;Lager2;Ort1_von_Lager2
234
P1001;Teil 1001;Lager1;Ort1_von_Lager1
235
P1002;Teil 1002;Lager2;Ort1_von_Lager1
236
P1003;Teil 1003;Lager2;kein Lagerort
237
EOL
238
$entries = test_import($file,$settings2);
239
$entry = $entries->[0];
240
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
241
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
242
is $entry->{object}->bin_id, $bin2_1->id, 'Lagerort1';
243
$entry = $entries->[2];
244
my $err1 = @{ $entry->{errors} }[0];
245
#print "'".$err1."'\n";
246
is $entry->{object}->description, 'Teil 1002', 'Teil 1002 set';
247
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
248
is $err1, 'Error: Bin Ort1_von_Lager1 is not from warehouse Lager2','kein Lager von Lager2';
249
$entry = $entries->[3];
250
$err1 = @{ $entry->{errors} }[0];
251
#print "'".$err1."'\n";
252
is $entry->{object}->description, 'Teil 1003', 'Teil 1003 set';
253
is $entry->{object}->warehouse_id, $wh2->id, 'Lager2';
254
is $err1, 'Error: Invalid bin name kein Lagerort','kein Lagerort';
255

  
256
##### add translations
257
$file = \<<EOL;
258
partnumber;description;description_EN;notes_EN;description_IT;notes_IT
259
P1000;Teil 1000;descr EN 1000;notes EN;descr IT 1000;notes IT
260
P1001;Teil 1001;descr EN 1001;notes EN;descr IT 1001;notes IT
261
P1002;Teil 1002;descr EN 1002;notes EN;descr IT 1002;notes IT
262
P1003;Teil 1003;descr EN 1003;notes EN;descr IT 1003;notes IT
263
EOL
264
$entries = test_import($file,$settings2);
265
$entry = $entries->[0];
266
is $entry->{object}->description, 'Teil 1000', 'Teil 1000 set';
267
is $entry->{raw_data}->{description_EN},'descr EN 1000','EN set';
268
is $entry->{raw_data}->{description_IT},'descr IT 1000','IT set';
269
my $l = @{$entry->{object}->translations}[0];
270
is $l->translation,'descr EN 1000','EN trans set';
271
is $l->longdescription, 'notes EN','EN notes set';
272
$l = @{$entry->{object}->translations}[1];
273
is $l->translation,'descr IT 1000','IT trans set';
274
is $l->longdescription, 'notes IT','IT notes set';
275

  
276
##### add customvar
277
$file = \<<EOL;
278
partnumber;cvar_mycvar
279
P1000;das ist der ring
280
P1001;nicht der nibelungen
281
P1002;sondern vom
282
P1003;Herr der Ringe
283
EOL
284
$entries = test_import($file,$settings2);
285
$entry = $entries->[0];
286
is $entry->{object}->partnumber, 'P1000', 'P1000 set';
287
is $entry->{raw_data}->{cvar_mycvar},'das ist der ring','CVAR set';
288
is @{$entry->{object}->custom_variables}[0]->text_value,'das ist der ring','Cvar mit richtigem Weert';
289

  
290
clear_up(); # remove all data at end of tests
291

  
292
# end of tests
293

  
294

  
295
sub clear_up {
296
  SL::DB::Manager::Part       ->delete_all(all => 1);
297
  SL::DB::Manager::Translation->delete_all(all => 1);
298
  SL::DB::Manager::Language   ->delete_all(all => 1);
299
  SL::DB::Manager::Bin        ->delete_all(all => 1);
300
  SL::DB::Manager::Warehouse  ->delete_all(all => 1);
301
  SL::DB::Manager::CustomVariableConfig->delete_all(all => 1);
302
}
303

  
304

  
305
1;
306

  
307
#####
308
# vim: ft=perl
309
# set emacs to perl mode
310
# Local Variables:
311
# mode: perl
312
# End:
templates/webpages/csv_import/_form_parts.html
3 3
<tr>
4 4
 <th align="right">[%- LxERP.t8('Parts with existing part numbers') %]:</th>
5 5
 <td colspan="10">
6
  [% opts = [ [ 'update_prices', LxERP.t8('Update prices of existing entries') ], [ 'insert_new', LxERP.t8('Insert with new part number') ], [ 'skip', LxERP.t8('Skip entry') ] ] %]
6
  [% 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') ] ] %]
7 7
  [% L.select_tag('settings.article_number_policy', opts, default = SELF.profile.get('article_number_policy'), style = 'width: 300px') %]
8 8
 </td>
9 9
</tr>

Auch abrufbar als: Unified diff