Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 6f45549b

Von Bernd Bleßmann vor fast 11 Jahren hinzugefügt

  • ID 6f45549b6f3d1f4f178b8799c9997fdc6e1447d0
  • Vorgänger ffb54c7e
  • Nachfolger 69f73331

Auftrags-Import

Ändert den Controller, dass er mit Multiplex-Daten umgehen kann.
Neue Klasse BaseMulti für Mulitplex-Daten (abgeleitet von Base).
Neue Klasse Order für Auftrags-Import (abgeleitet von BaseMulti).
Eintrag im Menü.
Anpassungen der templates.

Unterschiede anzeigen:

SL/Controller/CsvImport.pm
15 15
use SL::Controller::CsvImport::Part;
16 16
use SL::Controller::CsvImport::Shipto;
17 17
use SL::Controller::CsvImport::Project;
18
use SL::Controller::CsvImport::Order;
18 19
use SL::BackgroundJob::CsvImport;
19 20
use SL::System::TaskServer;
20 21

  
......
125 126
  my $file      = SL::SessionFile->new($file_name, mode => '>', encoding => $self->profile->get('charset'));
126 127
  my $csv       = Text::CSV_XS->new({ binary => 1, map { ( $_ => $self->profile->get($_) ) } qw(sep_char escape_char quote_char),});
127 128

  
128
  $csv->print($file->fh, [ map { $_->{name}        } @{ $self->displayable_columns } ]);
129
  $file->fh->print("\r\n");
130
  $csv->print($file->fh, [ map { $_->{description} } @{ $self->displayable_columns } ]);
131
  $file->fh->print("\r\n");
129
  if ($self->worker->is_multiplexed) {
130
    foreach my $ri (keys %{ $self->displayable_columns }) {
131
      $csv->print($file->fh, [ map { $_->{name}        } @{ $self->displayable_columns->{$ri} } ]);
132
      $file->fh->print("\r\n");
133
    }
134
    foreach my $ri (keys %{ $self->displayable_columns }) {
135
      $csv->print($file->fh, [ map { $_->{description} } @{ $self->displayable_columns->{$ri} } ]);
136
      $file->fh->print("\r\n");
137
    }
138
  } else {
139
    $csv->print($file->fh, [ map { $_->{name}        } @{ $self->displayable_columns } ]);
140
    $file->fh->print("\r\n");
141
    $csv->print($file->fh, [ map { $_->{description} } @{ $self->displayable_columns } ]);
142
    $file->fh->print("\r\n");
143
  }
132 144

  
133 145
  $file->fh->close;
134 146

  
......
158 170
                            : $page;
159 171
  $pages->{common}          = [ grep { $_->{visible} } @{ SL::DB::Helper::Paginated::make_common_pages($pages->{cur}, $pages->{max}) } ];
160 172

  
173
  $self->{report_numheaders} = $self->{report}->numheaders;
174
  my $first_row_header = 0;
175
  my $last_row_header  = $self->{report_numheaders} - 1;
176
  my $first_row_data   = $pages->{per_page} * ($pages->{cur}-1) + $self->{report_numheaders};
177
  my $last_row_data    = min($pages->{per_page} * $pages->{cur}, $num_rows) + $self->{report_numheaders} - 1;
161 178
  $self->{display_rows} = [
162
    0,
163
    $pages->{per_page} * ($pages->{cur}-1) + 1
179
    $first_row_header
180
      ..
181
    $last_row_header,
182
    $first_row_data
164 183
      ..
165
    min($pages->{per_page} * $pages->{cur}, $num_rows)
184
    $last_row_data
166 185
  ];
167 186

  
168 187
  my @query = (
169 188
    csv_import_report_id => $report_id,
170 189
    or => [
171
      row => 0,
172 190
      and => [
173
        row => { gt => $pages->{per_page} * ($pages->{cur}-1) },
174
        row => { le => $pages->{per_page} * $pages->{cur} },
191
        row => { ge => $first_row_header },
192
        row => { le => $last_row_header },
193
      ],
194
      and => [
195
        row => { ge => $first_row_data },
196
        row => { le => $last_row_data },
175 197
      ]
176 198
    ]
177 199
  );
......
199 221
sub check_type {
200 222
  my ($self) = @_;
201 223

  
202
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts customers_vendors addresses contacts projects);
224
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts customers_vendors addresses contacts projects orders);
203 225
  $self->type($::form->{profile}->{type});
204 226
}
205 227

  
......
242 264
            : $self->type eq 'contacts'          ? $::locale->text('CSV import: contacts')
243 265
            : $self->type eq 'parts'             ? $::locale->text('CSV import: parts and services')
244 266
            : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
267
            : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
245 268
            : die;
246 269

  
247 270
  if ($self->{type} eq 'parts') {
......
363 386
    $::form->{settings}->{sellprice_adjustment} = $::form->parse_amount(\%::myconfig, $::form->{settings}->{sellprice_adjustment});
364 387
  }
365 388

  
389
  if ($self->type eq 'orders') {
390
    $::form->{settings}->{order_column} = 'Order';
391
    $::form->{settings}->{item_column}  = 'OrderItem';
392
  }
393

  
366 394
  delete $::form->{profile}->{id};
367 395
  $self->profile($existing_profile || SL::DB::CsvImportProfile->new(login => $::myconfig{login}));
368 396
  $self->profile->assign_attributes(%{ $::form->{profile} });
......
389 417
sub save_report {
390 418
  my ($self, $report_id) = @_;
391 419

  
420
  if ($self->worker->is_multiplexed) {
421
    return $self->save_report_multi($report_id);
422
  } else {
423
    return $self->save_report_single($report_id);
424
  }
425
}
426

  
427
sub save_report_single {
428
  my ($self, $report_id) = @_;
429

  
392 430
  $self->track_progress(phase => 'building report', progress => 0);
393 431

  
394 432
  my $clone_profile = $self->profile->clone_and_reset_deep;
......
400 438
    type       => $self->type,
401 439
    file       => '',
402 440
    numrows    => scalar @{ $self->data },
441
    numheaders => 1,
403 442
  );
404 443

  
405 444
  $report->save(cascade => 1) or die $report->db->error;
......
455 494
  return $report->id;
456 495
}
457 496

  
497
sub save_report_multi {
498
  my ($self, $report_id) = @_;
499

  
500
  $self->track_progress(phase => 'building report', progress => 0);
501

  
502
  my $clone_profile = $self->profile->clone_and_reset_deep;
503
  $clone_profile->save; # weird bug. if this isn't saved before adding it to the report, it will default back to the last profile.
504

  
505
  my $report = SL::DB::CsvImportReport->new(
506
    session_id => $::auth->create_or_refresh_session,
507
    profile    => $clone_profile,
508
    type       => $self->type,
509
    file       => '',
510
    numrows    => scalar @{ $self->data },
511
    numheaders => scalar @{ $self->worker->profile },
512
  );
513

  
514
  $report->save(cascade => 1) or die $report->db->error;
515

  
516
  my $dbh = $::form->get_standard_dbh;
517
  $dbh->begin_work;
518

  
519
  my $query  = 'INSERT INTO csv_import_report_rows (csv_import_report_id, col, row, value) VALUES (?, ?, ?, ?)';
520
  my $query2 = 'INSERT INTO csv_import_report_status (csv_import_report_id, row, type, value) VALUES (?, ?, ?, ?)';
521

  
522
  my $sth = $dbh->prepare($query);
523
  my $sth2 = $dbh->prepare($query2);
524

  
525
  # save headers
526
  my ($headers, $info_methods, $raw_methods, $methods);
527

  
528
  for my $i (0 .. $#{ $self->worker->profile }) {
529
    my $row_ident = $self->worker->profile->[$i]->{row_ident};
530

  
531
    for my $i (0 .. $#{ $self->info_headers->{$row_ident}->{headers} }) {
532
      next unless                            $self->info_headers->{$row_ident}->{used}->{ $self->info_headers->{$row_ident}->{methods}->[$i] };
533
      push @{ $headers->{$row_ident} },      $self->info_headers->{$row_ident}->{headers}->[$i];
534
      push @{ $info_methods->{$row_ident} }, $self->info_headers->{$row_ident}->{methods}->[$i];
535
    }
536
    for my $i (0 .. $#{ $self->headers->{$row_ident}->{headers} }) {
537
      next unless                       $self->headers->{$row_ident}->{used}->{ $self->headers->{$row_ident}->{headers}->[$i] };
538
      push @{ $headers->{$row_ident} }, $self->headers->{$row_ident}->{headers}->[$i];
539
      push @{ $methods->{$row_ident} }, $self->headers->{$row_ident}->{methods}->[$i];
540
    }
541

  
542
    for my $i (0 .. $#{ $self->raw_data_headers->{$row_ident}->{headers} }) {
543
    next unless                           $self->raw_data_headers->{$row_ident}->{used}->{ $self->raw_data_headers->{$row_ident}->{headers}->[$i] };
544
    push @{ $headers->{$row_ident} },     $self->raw_data_headers->{$row_ident}->{headers}->[$i];
545
    push @{ $raw_methods->{$row_ident} }, $self->raw_data_headers->{$row_ident}->{headers}->[$i];
546
  }
547

  
548
  }
549

  
550
  for my $i (0 .. $#{ $self->worker->profile }) {
551
    my $row_ident = $self->worker->profile->[$i]->{row_ident};
552
    $sth->execute($report->id, $_, $i, $headers->{$row_ident}->[$_]) for 0 .. $#{ $headers->{$row_ident} };
553
  }
554

  
555
  # col offsets
556
  my ($off1, $off2);
557
  for my $i (0 .. $#{ $self->worker->profile }) {
558
    my $row_ident = $self->worker->profile->[$i]->{row_ident};
559
    my $n_info_methods = $info_methods->{$row_ident} ? scalar @{ $info_methods->{$row_ident} } : 0;
560
    my $n_methods      = $methods->{$row_ident} ?      scalar @{ $methods->{$row_ident} }      : 0;
561
    
562
    $off1->{$row_ident} = $n_info_methods;
563
    $off2->{$row_ident} = $off1->{$row_ident} + $n_methods;
564
  }
565

  
566
  my $n_header_rows = scalar @{ $self->worker->profile };
567

  
568
  for my $row (0 .. $#{ $self->data }) {
569
    $self->track_progress(progress => $row / @{ $self->data } * 100) if $row % 1000 == 0;
570
    my $data_row = $self->{data}[$row];
571
    my $row_ident = $data_row->{raw_data}{datatype};
572

  
573
    my $o1 = $off1->{$row_ident};
574
    my $o2 = $off2->{$row_ident};
575
    
576
    $sth->execute($report->id,       $_, $row + $n_header_rows, $data_row->{info_data}{ $info_methods->{$row_ident}->[$_] }) for 0 .. $#{ $info_methods->{$row_ident} };
577
    $sth->execute($report->id, $o1 + $_, $row + $n_header_rows, $data_row->{object}->${ \ $methods->{$row_ident}->[$_] })    for 0 .. $#{ $methods->{$row_ident} };
578
    $sth->execute($report->id, $o2 + $_, $row + $n_header_rows, $data_row->{raw_data}{ $raw_methods->{$row_ident}->[$_] })   for 0 .. $#{ $raw_methods->{$row_ident} };
579

  
580
    $sth2->execute($report->id, $row + $n_header_rows, 'information', $_) for @{ $data_row->{information} || [] };
581
    $sth2->execute($report->id, $row + $n_header_rows, 'errors', $_)      for @{ $data_row->{errors}      || [] };
582
  }
583

  
584
  $dbh->commit;
585

  
586
  return $report->id;
587
}
588

  
458 589
sub csv_file_name {
459 590
  my ($self) = @_;
460 591
  return "csv-import-" . $self->type . ".csv";
......
474 605
       : $self->{type} eq 'addresses'         ? SL::Controller::CsvImport::Shipto->new(@args)
475 606
       : $self->{type} eq 'parts'             ? SL::Controller::CsvImport::Part->new(@args)
476 607
       : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
608
       : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
477 609
       :                                        die "Program logic error";
478 610
}
479 611

  
SL/Controller/CsvImport/Base.pm
18 18
use Rose::Object::MakeMethods::Generic
19 19
(
20 20
 scalar                  => [ qw(controller file csv test_run save_with_cascade) ],
21
 'scalar --get_set_init' => [ qw(profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by all_currencies default_currency_id all_vc vc_by) ],
21
 'scalar --get_set_init' => [ qw(is_multiplexed profile displayable_columns existing_objects class manager_class cvar_columns all_cvar_configs all_languages payment_terms_by all_currencies default_currency_id all_vc vc_by) ],
22 22
);
23 23

  
24 24
sub run {
......
311 311
  $self->manager_class("SL::DB::Manager::" . $1);
312 312
}
313 313

  
314
sub init_is_multiplexed {
315
  my ($self) = @_;
316

  
317
  $self->is_multiplexed('ARRAY' eq ref ($self->class) && scalar @{ $self->class } > 1);
318
}
319

  
314 320
sub check_objects {
315 321
}
316 322

  
SL/Controller/CsvImport/BaseMulti.pm
1
package SL::Controller::CsvImport::BaseMulti;
2

  
3
use strict;
4

  
5
use List::MoreUtils qw(pairwise);
6

  
7
use SL::Helper::Csv;
8
use SL::DB::Customer;
9
use SL::DB::Language;
10
use SL::DB::PaymentTerm;
11
use SL::DB::Vendor;
12
use SL::DB::Contact;
13

  
14
use parent qw(SL::Controller::CsvImport::Base);
15

  
16
sub run {
17
  my ($self, %params) = @_;
18

  
19
  $self->test_run($params{test_run});
20

  
21
  $self->controller->track_progress(phase => 'parsing csv', progress => 0);
22

  
23
  my $profile = $self->profile;
24

  
25
  $self->csv(SL::Helper::Csv->new(file                    => $self->file->file_name,
26
                                  encoding                => $self->controller->profile->get('charset'),
27
                                  profile                 => $profile,
28
                                  ignore_unknown_columns  => 1,
29
                                  strict_profile          => 1,
30
                                  case_insensitive_header => 1,
31
                                  map { ( $_ => $self->controller->profile->get($_) ) } qw(sep_char escape_char quote_char),
32
                                 ));
33

  
34
  $self->controller->track_progress(progress => 10);
35

  
36
  my $old_numberformat      = $::myconfig{numberformat};
37
  $::myconfig{numberformat} = $self->controller->profile->get('numberformat');
38

  
39
  $self->csv->parse;
40

  
41
  $self->controller->track_progress(progress => 50);
42

  
43
  # bb: make sanity-check of it?
44
  #if ($self->csv->is_multiplexed != $self->is_multiplexed) {
45
  #  die "multiplex controller on simplex data or vice versa";
46
  #}
47

  
48
  $self->controller->errors([ $self->csv->errors ]) if $self->csv->errors;
49

  
50
  return if ( !$self->csv->header || $self->csv->errors );
51

  
52
  my $headers;
53
  my $i = 0;
54
  foreach my $header (@{ $self->csv->header }) {
55

  
56
    my $profile   = $self->csv->profile->[$i]->{profile};
57
    my $row_ident = $self->csv->profile->[$i]->{row_ident};
58

  
59
    my $h = { headers => [ grep { $profile->{$_} } @{ $header } ] };
60
    $h->{methods} = [ map { $profile->{$_} } @{ $h->{headers} } ];
61
    $h->{used}    = { map { ($_ => 1) }      @{ $h->{headers} } };
62

  
63
    $headers->{$row_ident} = $h;
64
    $i++;
65
  }
66

  
67
  $self->controller->headers($headers);
68

  
69
  my $raw_data_headers;
70
  my $info_headers;
71
  foreach my $p (@{ $self->csv->profile }) {
72
    $raw_data_headers->{ $p->{row_ident} } = { used => { }, headers => [ ] };
73
    $info_headers->{ $p->{row_ident} }     = { used => { }, headers => [ ] };
74
  }
75
  $self->controller->raw_data_headers($raw_data_headers);
76
  $self->controller->info_headers($info_headers);
77
    
78

  
79
  my @objects  = $self->csv->get_objects;
80
  $self->controller->track_progress(progress => 70);
81

  
82
  my @raw_data = @{ $self->csv->get_data };
83

  
84
  $self->controller->track_progress(progress => 80);
85

  
86
  $self->controller->data([ pairwise { { object => $a, raw_data => $b, errors => [], information => [], info_data => {} } } @objects, @raw_data ]);
87

  
88
  $self->controller->track_progress(progress => 90);
89

  
90
  $self->check_objects;
91
  if ( $self->controller->profile->get('duplicates', 'no_check') ne 'no_check' ) {
92
    $self->check_std_duplicates();
93
    $self->check_duplicates();
94
  }
95
  $self->fix_field_lengths;
96

  
97
  $self->controller->track_progress(progress => 100);
98

  
99
  $::myconfig{numberformat} = $old_numberformat;
100
}
101

  
102
sub add_columns {
103
  my ($self, $row_ident, @columns) = @_;
104
  
105
  my $h = $self->controller->headers->{$row_ident};
106

  
107
  foreach my $column (grep { !$h->{used}->{$_} } @columns) {
108
    $h->{used}->{$column} = 1;
109
    push @{ $h->{methods} }, $column;
110
    push @{ $h->{headers} }, $column;
111
  }
112
}
113

  
114
sub add_info_columns {
115
  my ($self, $row_ident, @columns) = @_;
116

  
117
  my $h = $self->controller->info_headers->{$row_ident};
118

  
119
  foreach my $column (grep { !$h->{used}->{ $_->{method} } } map { ref $_ eq 'HASH' ? $_ : { method => $_, header => $_ } } @columns) {
120
    $h->{used}->{ $column->{method} } = 1;
121
    push @{ $h->{methods} }, $column->{method};
122
    push @{ $h->{headers} }, $column->{header};
123
  }
124
}
125

  
126
sub add_raw_data_columns {
127
  my ($self, $row_ident, @columns) = @_;
128

  
129
  my $h = $self->controller->raw_data_headers->{$row_ident};
130

  
131
  foreach my $column (grep { !$h->{used}->{$_} } @columns) {
132
    $h->{used}->{$column} = 1;
133
    push @{ $h->{headers} }, $column;
134
  }
135
}
136

  
137
sub add_cvar_raw_data_columns {
138
  my ($self) = @_;
139

  
140
  map { $self->add_raw_data_columns($_) if exists $self->controller->data->[0]->{raw_data}->{$_} } @{ $self->cvar_columns };
141
}
142

  
143
sub init_profile {
144
  my ($self) = @_;
145

  
146
  my @profile;
147
  foreach my $class (@{ $self->class }) {
148
    eval "require " . $class;
149

  
150
    my %unwanted = map { ( $_ => 1 ) } (qw(itime mtime), map { $_->name } @{ $class->meta->primary_key_columns });
151
    my %prof;
152
    $prof{datatype} = '';
153
    for my $col ($class->meta->columns) {
154
      next if $unwanted{$col};
155

  
156
      my $name = $col->isa('Rose::DB::Object::Metadata::Column::Numeric')   ? "$col\_as_number"
157
          :      $col->isa('Rose::DB::Object::Metadata::Column::Date')      ? "$col\_as_date"
158
          :      $col->isa('Rose::DB::Object::Metadata::Column::Timestamp') ? "$col\_as_date"
159
          :                                                                   $col->name;
160

  
161
      $prof{$col} = $name;
162
    }
163

  
164
    $prof{ 'cvar_' . $_->name } = '' for @{ $self->all_cvar_configs };
165

  
166
    $class =~ m/^SL::DB::(.+)/;
167
    push @profile, {'profile' => \%prof, 'class' => $class, 'row_ident' => $1};
168
  }
169

  
170
  \@profile;
171
}
172

  
173
sub add_displayable_columns {
174
  my ($self, $row_ident, @columns) = @_;
175

  
176
  my $dis_cols = $self->controller->displayable_columns || {};
177

  
178
  my @cols       = @{ $dis_cols->{$row_ident} || [] };
179
  my %ex_col_map = map { $_->{name} => $_ } @cols;
180

  
181
  foreach my $column (@columns) {
182
    if ($ex_col_map{ $column->{name} }) {
183
      @{ $ex_col_map{ $column->{name} } }{ keys %{ $column } } = @{ $column }{ keys %{ $column } };
184
    } else {
185
      push @cols, $column;
186
    }
187
  }
188

  
189
  my $by_name_datatype_first = sub { 'datatype' eq $a->{name} ? -1 :
190
                                     'datatype' eq $b->{name} ?  1 :
191
                                     $a->{name} cmp $b->{name} };
192
  $dis_cols->{$row_ident} = [ sort $by_name_datatype_first @cols ];
193

  
194
  $self->controller->displayable_columns($dis_cols);
195
}
196

  
197
sub setup_displayable_columns {
198
  my ($self) = @_;
199

  
200
  foreach my $p (@{ $self->profile }) {
201
    $self->add_displayable_columns($p->{row_ident}, map { { name => $_ } } keys %{ $p->{profile} });
202
  }
203
}
204

  
205
sub add_cvar_columns_to_displayable_columns {
206
  my ($self) = @_;
207

  
208
  $self->add_displayable_columns(map { { name        => 'cvar_' . $_->name,
209
                                         description => $::locale->text('#1 (custom variable)', $_->description) } }
210
                                     @{ $self->all_cvar_configs });
211
}
212

  
213
sub init_existing_objects {
214
  my ($self) = @_;
215

  
216
  eval "require " . $self->class;
217
  $self->existing_objects($self->manager_class->get_all);
218
}
219

  
220
sub init_class {
221
  die "class not set";
222
}
223

  
224
sub init_manager_class {
225
  my ($self) = @_;
226

  
227
  $self->class =~ m/^SL::DB::(.+)/;
228
  $self->manager_class("SL::DB::Manager::" . $1);
229
}
230

  
231
1;
232

  
SL/Controller/CsvImport/Order.pm
1
package SL::Controller::CsvImport::Order;
2

  
3

  
4
use strict;
5

  
6
use List::MoreUtils qw(any);
7

  
8
use SL::Helper::Csv;
9
use SL::DB::Order;
10
use SL::DB::OrderItem;
11
use SL::DB::Part;
12
use SL::DB::PaymentTerm;
13
use SL::DB::Contact;
14

  
15
use parent qw(SL::Controller::CsvImport::BaseMulti);
16

  
17

  
18
use Rose::Object::MakeMethods::Generic
19
(
20
 'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by all_contacts contacts_by) ],
21
);
22

  
23

  
24
sub init_class {
25
  my ($self) = @_;
26
  $self->class(['SL::DB::Order', 'SL::DB::OrderItem']);
27
}
28

  
29

  
30
sub init_settings {
31
  my ($self) = @_;
32

  
33
  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(order_column item_column) };
34
}
35

  
36

  
37
sub init_profile {
38
  my ($self) = @_;
39

  
40
  my $profile = $self->SUPER::init_profile;
41

  
42
  foreach my $p (@{ $profile }) {
43
    my $prof = $p->{profile};
44
    if ($p->{row_ident} eq 'Order') {
45
      # no need to handle
46
      delete @{$prof}{qw(delivery_customer_id delivery_vendor_id proforma quotation amount netamount)};
47
      # handable, but not handled by now
48
    }
49
    if ($p->{row_ident} eq 'OrderItem') {
50
      delete @{$prof}{qw(trans_id)};
51
    }
52
  }
53

  
54
  return $profile;
55
}
56

  
57

  
58
sub setup_displayable_columns {
59
  my ($self) = @_;
60

  
61
  $self->SUPER::setup_displayable_columns;
62

  
63
  $self->add_displayable_columns('Order',
64
                                 { name => 'datatype',         description => $::locale->text('Zeilenkennung')                  },
65
                                 { name => 'verify_amount',    description => $::locale->text('Amount (for verification)')      },
66
                                 { name => 'verify_netamount', description => $::locale->text('Net amount (for verification)')  },
67
                                 { name => 'taxincluded',      description => $::locale->text('Tax Included')                   },
68
                                 { name => 'customer',         description => $::locale->text('Customer (name)')                },
69
                                 { name => 'customernumber',   description => $::locale->text('Customer Number')                },
70
                                 { name => 'customer_id',      description => $::locale->text('Customer (database ID)')         },
71
                                 { name => 'vendor',           description => $::locale->text('Vendor (name)')                  },
72
                                 { name => 'vendornumber',     description => $::locale->text('Vendor Number')                  },
73
                                 { name => 'vendor_id',        description => $::locale->text('Vendor (database ID)')           },
74
                                 { name => 'language_id',      description => $::locale->text('Language (database ID)')         },
75
                                 { name => 'language',         description => $::locale->text('Language (name)')                },
76
                                 { name => 'payment_id',       description => $::locale->text('Payment terms (database ID)')    },
77
                                 { name => 'payment',          description => $::locale->text('Payment terms (name)')           },
78
                                 { name => 'taxzone_id',       description => $::locale->text('Steuersatz')                     },
79
                                 { name => 'contact_id',       description => $::locale->text('Contact Person (database ID)')   },
80
                                 { name => 'contact',          description => $::locale->text('Contact Person (name)')          },
81
                                );
82

  
83
  $self->add_displayable_columns('OrderItem',
84
                                 { name => 'parts_id',       description => $::locale->text('Part (database ID)')          },
85
                                 { name => 'partnumber',     description => $::locale->text('Part Number')                 },
86
                                );
87
}
88

  
89

  
90
sub init_languages_by {
91
  my ($self) = @_;
92

  
93
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
94
}
95

  
96
sub init_all_parts {
97
  my ($self) = @_;
98

  
99
  return SL::DB::Manager::Part->get_all;
100
}
101

  
102
sub init_parts_by {
103
  my ($self) = @_;
104

  
105
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
106
}
107

  
108
sub init_all_contacts {
109
  my ($self) = @_;
110

  
111
  return SL::DB::Manager::Contact->get_all;
112
}
113

  
114
sub init_contacts_by {
115
  my ($self) = @_;
116

  
117
  my $cby = { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_contacts } } ) } qw(cp_id cp_name) };
118

  
119
  # by customer/vendor id  _and_  contact person id
120
  $cby->{'cp_cv_id+cp_id'} = { map { ( $_->cp_cv_id . '+' . $_->cp_id => $_ ) } @{ $self->all_contacts } };
121

  
122
  return $cby;
123
}
124

  
125
sub check_objects {
126
  my ($self) = @_;
127

  
128
  $self->controller->track_progress(phase => 'building data', progress => 0);
129

  
130
  my $i;
131
  my $num_data = scalar @{ $self->controller->data };
132
  foreach my $entry (@{ $self->controller->data }) {
133
    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
134

  
135
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
136

  
137
      my $vc_obj;
138
      if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
139
        $self->check_vc($entry, 'customer_id');
140
        $vc_obj = SL::DB::Customer->new(id => $entry->{object}->customer_id)->load if $entry->{object}->customer_id;
141
      } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
142
        $self->check_vc($entry, 'vendor_id');
143
        $vc_obj = SL::DB::Vendor->new(id => $entry->{object}->vendor_id)->load if $entry->{object}->vendor_id;
144
      } else {
145
        push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
146
      }
147

  
148
      $self->check_contact($entry);
149
      $self->check_language($entry);
150
      $self->check_payment($entry);
151

  
152
      if ($vc_obj) {
153
        # copy from customer if not given
154
        foreach (qw(payment_id language_id taxzone_id)) {
155
          $entry->{object}->$_($vc_obj->$_) unless $entry->{object}->$_;
156
        }
157
      }
158

  
159
      # ToDo: salesman and emloyee by name
160
      # salesman from customer or login if not given
161
      if (!$entry->{object}->salesman) {
162
        if ($vc_obj && $vc_obj->salesman_id) {
163
          $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(id => $vc_obj->salesman_id));
164
        } else {
165
          $entry->{object}->salesman(SL::DB::Manager::Employee->find_by(login => $::myconfig{login}));
166
        }
167
      }
168

  
169
      # employee from login if not given
170
      if (!$entry->{object}->employee_id) {
171
        $entry->{object}->employee_id(SL::DB::Manager::Employee->find_by(login => $::myconfig{login})->id);
172
      }
173

  
174
    }
175
  }
176

  
177
  $self->add_info_columns($self->settings->{'order_column'},
178
                          { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
179
  $self->add_columns($self->settings->{'order_column'},
180
                     map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(business payment));
181

  
182

  
183
  foreach my $entry (@{ $self->controller->data }) {
184
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} && $entry->{object}->can('part')) {
185

  
186
      next if !$self->check_part($entry);
187

  
188
      my $part_obj = SL::DB::Part->new(id => $entry->{object}->parts_id)->load;
189

  
190
      # copy from part if not given
191
      $entry->{object}->description($part_obj->description) unless $entry->{object}->description;
192
      $entry->{object}->longdescription($part_obj->notes)   unless $entry->{object}->longdescription;
193
      $entry->{object}->unit($part_obj->unit)               unless $entry->{object}->unit;
194

  
195
      # set to 0 if not given
196
      $entry->{object}->discount(0)      unless $entry->{object}->discount;
197
      $entry->{object}->ship(0)          unless $entry->{object}->ship;
198
    }
199
  }
200

  
201
  $self->add_info_columns($self->settings->{'item_column'},
202
                          { header => $::locale->text('Part Number'), method => 'partnumber' });
203

  
204
  # add orderitems to order
205
  my $order_entry;
206
  my @orderitems;
207
  foreach my $entry (@{ $self->controller->data }) {
208
    # search first Order
209
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
210

  
211
      # new order entry: add collected orderitems to the last one
212
      if (defined $order_entry) {
213
        $order_entry->{object}->orderitems(@orderitems);
214
        @orderitems = ();
215
      }
216

  
217
      $order_entry = $entry;
218

  
219
    } elsif ( defined $order_entry && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'} ) {
220
      # collect orderitems to add to order (if they have no errors)
221
      # ( add_orderitems does not work here if we want to call
222
      #   calculate_prices_and_taxes afterwards ...
223
      #   so collect orderitems and add them at once)
224
      if (scalar @{ $entry->{errors} } == 0) {
225
        push @orderitems, $entry->{object};
226
      }
227
    }
228
  }
229
  # add last collected orderitems to last order
230
  if ($order_entry) {
231
    $order_entry->{object}->orderitems(@orderitems);
232
  }
233

  
234
  # calculate prices and taxes
235
  foreach my $entry (@{ $self->controller->data }) {
236
    next if @{ $entry->{errors} };
237

  
238
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
239

  
240
      $entry->{object}->calculate_prices_and_taxes;
241

  
242
      $entry->{info_data}->{calc_amount}    = $entry->{object}->amount_as_number;
243
      $entry->{info_data}->{calc_netamount} = $entry->{object}->netamount_as_number;
244
    }
245
  }
246

  
247
  # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
248
  # And throw an error if the differences are too big.
249
  my $max_diff = 0.02;
250
  my @to_verify = ( { column      => 'amount',
251
                      raw_column  => 'verify_amount',
252
                      info_header => 'Calc. Amount',
253
                      info_method => 'calc_amount',
254
                      err_msg     => 'Amounts differ too much',
255
                    },
256
                    { column      => 'netamount',
257
                      raw_column  => 'verify_netamount',
258
                      info_header => 'Calc. Net amount',
259
                      info_method => 'calc_netamount',
260
                      err_msg     => 'Net amounts differ too much',
261
                    } );
262

  
263
  foreach my $tv (@to_verify) {
264
    if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
265
      $self->add_raw_data_columns($self->settings->{'order_column'}, $tv->{raw_column});
266
      $self->add_info_columns($self->settings->{'order_column'},
267
                              { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
268
    }
269

  
270
    # check differences
271
    foreach my $entry (@{ $self->controller->data }) {
272
      next if @{ $entry->{errors} };
273
      if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
274
        next if !$entry->{raw_data}->{ $tv->{raw_column} };
275
        my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
276
        if (abs($entry->{object}->${ \$tv->{column} } - $parsed_value) > $max_diff) {
277
          push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
278
        }
279
      }
280
    }
281
  }
282

  
283
  # If order has errors set error for orderitems as well
284
  my $order_entry;
285
  foreach my $entry (@{ $self->controller->data }) {
286
    # Search first order
287
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'}) {
288
      $order_entry = $entry;
289
    } elsif ( defined $order_entry
290
              && $entry->{raw_data}->{datatype} eq $self->settings->{'item_column'}
291
              && scalar @{ $order_entry->{errors} } > 0 ) {
292
      push @{ $entry->{errors} }, $::locale->text('order not valid for this orderitem!');
293
    }
294
  }
295

  
296
}
297

  
298

  
299
sub check_language {
300
  my ($self, $entry) = @_;
301

  
302
  my $object = $entry->{object};
303

  
304
  # Check whether or not language ID is valid.
305
  if ($object->language_id && !$self->languages_by->{id}->{ $object->language_id }) {
306
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
307
    return 0;
308
  }
309

  
310
  # Map name to ID if given.
311
  if (!$object->language_id && $entry->{raw_data}->{language}) {
312
    my $language = $self->languages_by->{description}->{  $entry->{raw_data}->{language} }
313
                || $self->languages_by->{article_code}->{ $entry->{raw_data}->{language} };
314

  
315
    if (!$language) {
316
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
317
      return 0;
318
    }
319

  
320
    $object->language_id($language->id);
321
  }
322

  
323
  if ($object->language_id) {
324
    $entry->{info_data}->{language} = $self->languages_by->{id}->{ $object->language_id }->description;
325
  }
326

  
327
  return 1;
328
}
329

  
330
sub check_part {
331
  my ($self, $entry) = @_;
332

  
333
  my $object = $entry->{object};
334

  
335
  # Check wether or non part ID is valid.
336
  if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
337
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
338
    return 0;
339
  }
340

  
341
  # Map number to ID if given.
342
  if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
343
    my $part = $self->parts_by->{partnumber}->{ $entry->{raw_data}->{partnumber} };
344
    if (!$part) {
345
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid part');
346
      return 0;
347
    }
348

  
349
    $object->parts_id($part->id);
350
  }
351

  
352
  if ($object->parts_id) {
353
    $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
354
  } else {
355
    push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
356
    return 0;
357
  }
358

  
359
  return 1;
360
}
361

  
362
sub check_contact {
363
  my ($self, $entry) = @_;
364

  
365
  my $object = $entry->{object};
366

  
367
  # Check wether or non contact ID is valid.
368
  if ($object->cp_id && !$self->contacts_by->{cp_id}->{ $object->cp_id }) {
369
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
370
    return 0;
371
  }
372

  
373
  # Map number to ID if given.
374
  if (!$object->cp_id && $entry->{raw_data}->{contact}) {
375
    my $cp = $self->contacts_by->{cp_name}->{ $entry->{raw_data}->{contact} };
376
    if (!$cp) {
377
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
378
      return 0;
379
    }
380

  
381
    $object->cp_id($cp->cp_id);
382
  }
383

  
384
  # Check if the contact belongs to this customer/vendor.
385
  if ($object->cp_id && $object->customer_id && !$self->contacts_by->{'cp_cv_id+cp_id'}) {
386
    push @{ $entry->{errors} }, $::locale->text('Error: Contact not found for this customer/vendor');
387
    return 0;
388
  }
389

  
390
  if ($object->cp_id) {
391
    $entry->{info_data}->{contact} = $self->contacts_by->{cp_id}->{ $object->cp_id }->cp_name;
392
  }
393

  
394
  return 1;
395
}
396

  
397
sub save_objects {
398
  my ($self, %params) = @_;
399

  
400
  # set order number and collect to save
401
  my $objects_to_save;
402
  foreach my $entry (@{ $self->controller->data }) {
403
    next if @{ $entry->{errors} };
404

  
405
    if ($entry->{raw_data}->{datatype} eq $self->settings->{'order_column'} && !$entry->{object}->ordnumber) {
406
      $entry->{object}->create_trans_number;
407
    }
408

  
409
    push @{ $objects_to_save }, $entry;
410
  }
411

  
412
  $self->SUPER::save_objects(data => $objects_to_save);
413
}
414

  
415

  
416
1;
menus/erp.ini
636 636
action=CsvImport/new
637 637
profile.type=projects
638 638

  
639
[System--Import CSV--Orders]
640
module=controller.pl
641
action=CsvImport/new
642
profile.type=orders
643

  
639 644
[System--Templates]
640 645
ACCESS=admin
641 646
module=menu.pl
templates/webpages/csv_import/_form_orders.html
1
[% USE LxERP %]
2
[% USE L %]
3
<tr>
4
 <th align="right">[%- LxERP.t8('Order/Item columns') %]:</th>
5
 <td colspan="10">
6
  [% L.input_tag('settings.order_column', SELF.profile.get('order_column'), size => "5") %]
7
  [% L.input_tag('settings.item_column',  SELF.profile.get('item_column'),  size => "5") %]
8
 </td>
9
</tr>
templates/webpages/csv_import/form.html
57 57
  <div class="help_toggle" style="display:none">
58 58
   <p><a href="#" onClick="javascript:$('.help_toggle').toggle()">[% LxERP.t8("Hide help text") %]</a></p>
59 59

  
60
   <table>
61
    <tr class="listheading">
62
     <th>[%- LxERP.t8('Column name') %]</th>
63
     <th>[%- LxERP.t8('Meaning') %]</th>
64
    </tr>
65

  
66
    [%- FOREACH row = SELF.displayable_columns %]
67
     <tr class="listrow[% loop.count % 2 %]">
68
      <td>[%- HTML.escape(row.name) %]</td>
69
      <td>[%- HTML.escape(row.description) %]</td>
70
     </tr>
71
    [%- END %]
72
   </table>
60
   [%- IF SELF.worker.is_multiplexed %]
61
     <table>
62
       <tr class="listheading">
63
         [%- FOREACH ri = SELF.displayable_columns.keys %]
64
           <th>[%- ri %]</th>
65
         [%- END %]
66
       </tr>
67
       <tr class="listrow[% loop.count % 2 %]">
68
         [%- FOREACH ri = SELF.displayable_columns.keys %]
69
         <td>
70
           <table>
71
             <tr class="listheading">
72
               <th>[%- LxERP.t8('Column name') %]</th>
73
               <th>[%- LxERP.t8('Meaning') %]</th>
74
             </tr>
75

  
76
             [%- FOREACH row = SELF.displayable_columns.$ri %]
77
             <tr class="listrow[% loop.count % 2 %]">
78
               <td>[%- HTML.escape(row.name) %]</td>
79
               <td>[%- HTML.escape(row.description) %]</td>
80
             </tr>
81
             [%- END %]
82
           </table>
83
         </td>
84
         [%- END %]
85
       </tr>
86
     </table>
87
   [%- ELSE %]
88
     <table>
89
       <tr class="listheading">
90
         <th>[%- LxERP.t8('Column name') %]</th>
91
         <th>[%- LxERP.t8('Meaning') %]</th>
92
       </tr>
93

  
94
       [%- FOREACH row = SELF.displayable_columns %]
95
       <tr class="listrow[% loop.count % 2 %]">
96
         <td>[%- HTML.escape(row.name) %]</td>
97
         <td>[%- HTML.escape(row.description) %]</td>
98
       </tr>
99
       [%- END %]
100
     </table>
101
   [%- END %]
73 102

  
74 103
[%- IF SELF.type == 'contacts' %]
75 104
   <p>
......
95 124
    [% LxERP.t8('The items are imported accoring do their number "X" regardless of the column order inside the file.') %]
96 125
    [% LxERP.t8('The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.') %]
97 126
   </p>
127

  
128
[%- ELSIF SELF.type == 'orders' %]
129
   <p>
130
     [%- LxERP.t8('Amount and net amount are calculated by kivitendo. verify_amount and verify_netamount can be used for sanity checks.') %]
131
   </p>
98 132
[%- END %]
99 133

  
100 134
   <p>
......
206 240
 [%- INCLUDE 'csv_import/_form_customers_vendors.html' %]
207 241
[%- ELSIF SELF.type == 'contacts' %]
208 242
 [%- INCLUDE 'csv_import/_form_contacts.html' %]
243
[%- ELSIF SELF.type == 'orders' %]
244
 [%- INCLUDE 'csv_import/_form_orders.html' %]
209 245
[%- END %]
210 246

  
211 247
   <tr>
templates/webpages/csv_import/report.html
6 6
[%- PROCESS 'common/paginate.html' pages=SELF.pages, base_url = SELF.base_url %]
7 7
 <table>
8 8
[%- FOREACH rownum = SELF.display_rows %]
9
 [%- IF loop.first %]
9
 [%- IF rownum < SELF.report_numheaders %]
10 10
  <tr class="listheading">
11 11
  [%- FOREACH value = SELF.report_rows.${rownum} %]
12 12
   <th>[% value | html %]</th>
......
21 21
  [%- END %]
22 22
   <td>
23 23
    [%- FOREACH error = csv_import_report_errors %][%- error | html %][% UNLESS loop.last %]<br>[%- END %][%- END %]
24
    [%- FOREACH info  = SELF.report_status.${rownum}.information %][% IF !loop.first || csv_import_report_errors.size %]<br>[%- END %][%- info | html %][%- END %]
24
    [%- FOREACH info  = SELF.report_status.${rownum}.information %][% IF rownum >= SELF.report_numheaders || csv_import_report_errors.size %]<br>[%- END %][%- info | html %][%- END %]
25 25
   </td>
26 26
  </tr>
27 27
 [%- END %]

Auch abrufbar als: Unified diff