Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 9f4ef62c

Von Bernd Bleßmann vor mehr als 4 Jahren hinzugefügt

  • ID 9f4ef62c42dceef5a27ad3a0d8c8d260ffef9848
  • Vorgänger 994dab13
  • Nachfolger 299dba8f

CsvImport für Lieferscheine

Unterschiede anzeigen:

SL/Controller/CsvImport.pm
20 20
use SL::Controller::CsvImport::Shipto;
21 21
use SL::Controller::CsvImport::Project;
22 22
use SL::Controller::CsvImport::Order;
23
use SL::Controller::CsvImport::DeliveryOrder;
23 24
use SL::Controller::CsvImport::ARTransaction;
24 25
use SL::JSON;
25 26
use SL::Controller::CsvImport::BankTransaction;
26 27
use SL::BackgroundJob::CsvImport;
27 28
use SL::System::TaskServer;
28 29

  
29
use List::MoreUtils qw(none);
30
use List::MoreUtils qw(any none);
30 31
use List::Util qw(min);
31 32

  
32 33
use parent qw(SL::Controller::Base);
......
306 307
sub check_type {
307 308
  my ($self) = @_;
308 309

  
309
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders bank_transactions ar_transactions);
310
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders delivery_orders bank_transactions ar_transactions);
310 311
  $self->type($::form->{profile}->{type});
311 312
}
312 313

  
......
353 354
            : $self->type eq 'inventories'       ? $::locale->text('CSV import: inventories')
354 355
            : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
355 356
            : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
357
            : $self->type eq 'delivery_orders'   ? $::locale->text('CSV import: delivery orders')
356 358
            : $self->type eq 'bank_transactions' ? $::locale->text('CSV import: bank transactions')
357 359
            : $self->type eq 'ar_transactions'   ? $::locale->text('CSV import: ar transactions')
358 360
            : die;
359 361

  
360
  if ($self->{type} eq 'customers_vendors' or $self->{type} eq 'orders' or $self->{type} eq 'ar_transactions' ) {
362
  if ( any { $_ eq $self->{type} } qw(customers_vendors orders delivery_orders ar_transactions) ) {
361 363
    $self->all_taxzones(SL::DB::Manager::TaxZone->get_all_sorted(query => [ obsolete => 0 ]));
362 364
  };
363 365

  
......
721 723
       : $self->{type} eq 'inventories'       ? SL::Controller::CsvImport::Inventory->new(@args)
722 724
       : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
723 725
       : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
726
       : $self->{type} eq 'delivery_orders'   ? SL::Controller::CsvImport::DeliveryOrder->new(@args)
724 727
       : $self->{type} eq 'bank_transactions' ? SL::Controller::CsvImport::BankTransaction->new(@args)
725 728
       : $self->{type} eq 'ar_transactions'   ? SL::Controller::CsvImport::ARTransaction->new(@args)
726 729
       :                                        die "Program logic error";
SL/Controller/CsvImport/Base.pm
551 551
          push @{ $entry->{errors} }, $::locale->text('Error when saving: #1', $object->db->error);
552 552
        } else {
553 553
          $self->_save_history($object);
554
          $self->save_additions($object);
554 555
          $self->controller->num_imported($self->controller->num_imported + 1);
555 556
        }
556 557
      }
......
592 593
  return @cleaned_fields;
593 594
}
594 595

  
596
sub save_additions {
597
  my ($self, $object) = @_;
598

  
599
  # Can be overridden by derived specialized importer classes to save
600
  # additional tables (e.g. record links).
601
  # This sub is called after the object is saved successfully in an transaction.
602

  
603
  return;
604
}
605

  
595 606
sub _save_history {
596 607
  my ($self, $object) = @_;
597 608

  
598
  if (any { $self->controller->{type} && $_ eq $self->controller->{type} } qw(parts customers_vendors orders ar_transactions)) {
609
  if (any { $self->controller->{type} && $_ eq $self->controller->{type} } qw(parts customers_vendors orders delivery_orders ar_transactions)) {
599 610
    my $snumbers = $self->controller->{type} eq 'parts'             ? 'partnumber_' . $object->partnumber
600 611
                 : $self->controller->{type} eq 'customers_vendors' ?
601 612
                     ($self->table eq 'customer' ? 'customernumber_' . $object->customernumber : 'vendornumber_' . $object->vendornumber)
602 613
                 : $self->controller->{type} eq 'orders'            ? 'ordnumber_' . $object->ordnumber
614
                 : $self->controller->{type} eq 'delivery_orders'   ? 'donumber_'  . $object->donumber
603 615
                 : $self->controller->{type} eq 'ar_transactions'   ? 'invnumber_' . $object->invnumber
604 616
                 : '';
605 617

  
......
607 619
    if ($self->controller->{type} eq 'orders') {
608 620
      $what_done = $object->customer_id ? 'sales_order' : 'purchase_order';
609 621
    }
622
    if ($self->controller->{type} eq 'delivery_orders') {
623
      $what_done = $object->customer_id ? 'sales_delivery_order' : 'purchase_delivery_order';
624
    }
610 625

  
611 626
    SL::DB::History->new(
612 627
      trans_id    => $object->id,
SL/Controller/CsvImport/BaseMulti.pm
94 94
  $::myconfig{numberformat} = $old_numberformat;
95 95
}
96 96

  
97
sub init_manager_class {
98
  my ($self) = @_;
99

  
100
  my @manager_classes;
101
  foreach my $class (@{ $self->class }) {
102
    $class =~ m/^SL::DB::(.+)/;
103
    push @manager_classes, "SL::DB::Manager::" . $1;
104
  }
105
  $self->manager_class(\@manager_classes);
106
}
107

  
97 108
sub add_columns {
98 109
  my ($self, $row_ident, @columns) = @_;
99 110

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

  
3

  
4
use strict;
5

  
6
use List::Util qw(first);
7
use List::MoreUtils qw(any none uniq);
8
use DateTime;
9

  
10
use SL::Controller::CsvImport::Helper::Consistency;
11
use SL::DB::DeliveryOrder;
12
use SL::DB::DeliveryOrderItem;
13
use SL::DB::DeliveryOrderItemsStock;
14
use SL::DB::Part;
15
use SL::DB::PaymentTerm;
16
use SL::DB::Contact;
17
use SL::DB::PriceFactor;
18
use SL::DB::Pricegroup;
19
use SL::DB::Shipto;
20
use SL::DB::Unit;
21
use SL::DB::Inventory;
22
use SL::DB::TransferType;
23
use SL::DBUtils;
24
use SL::PriceSource;
25
use SL::TransNumber;
26
use SL::Util qw(trim);
27

  
28
use parent qw(SL::Controller::CsvImport::BaseMulti);
29

  
30

  
31
use Rose::Object::MakeMethods::Generic
32
(
33
 'scalar --get_set_init' => [ qw(settings languages_by all_parts parts_by part_counts_by
34
                                 contacts_by ct_shiptos_by
35
                                 price_factors_by pricegroups_by units_by
36
                                 warehouses_by bins_by transfer_types_by) ],
37
);
38

  
39

  
40
sub init_class {
41
  my ($self) = @_;
42
  $self->class(['SL::DB::DeliveryOrder', 'SL::DB::DeliveryOrderItem', 'SL::DB::DeliveryOrderItemsStock']);
43
}
44

  
45
sub set_profile_defaults {
46
  my ($self) = @_;
47

  
48
  $self->controller->profile->_set_defaults(
49
    order_column         => $::locale->text('DeliveryOrder'),
50
    item_column          => $::locale->text('OrderItem'),
51
    stock_column         => $::locale->text('StockInfo'),
52
    ignore_faulty_positions => 0,
53
  );
54
};
55

  
56
sub init_settings {
57
  my ($self) = @_;
58

  
59
  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(order_column item_column stock_column ignore_faulty_positions) };
60
}
61

  
62
sub init_cvar_configs_by {
63
  my ($self) = @_;
64

  
65
  my $item_cvar_configs = SL::DB::Manager::CustomVariableConfig->get_all(where => [ module => 'IC' ]);
66
  $item_cvar_configs = [grep { $_->has_flag('editable') } @{ $item_cvar_configs }];
67

  
68
  my $ccb;
69
  $ccb->{class}->{$self->class->[0]}        = [];
70
  $ccb->{class}->{$self->class->[1]}        = $item_cvar_configs;
71
  $ccb->{class}->{$self->class->[2]}        = [];
72
  $ccb->{row_ident}->{$self->_order_column} = [];
73
  $ccb->{row_ident}->{$self->_item_column}  = $item_cvar_configs;
74
  $ccb->{row_ident}->{$self->_stock_column} = [];
75

  
76
  return $ccb;
77
}
78

  
79
sub init_profile {
80
  my ($self) = @_;
81

  
82
  my $profile = $self->SUPER::init_profile;
83

  
84
  # SUPER::init_profile sets row_ident to the translated class name
85
  # overwrite it with the user specified settings
86
  foreach my $p (@{ $profile }) {
87
    $p->{row_ident} = $self->_order_column if $p->{class} eq $self->class->[0];
88
    $p->{row_ident} = $self->_item_column  if $p->{class} eq $self->class->[1];
89
    $p->{row_ident} = $self->_stock_column if $p->{class} eq $self->class->[2];
90
  }
91

  
92
  foreach my $p (@{ $profile }) {
93
    my $prof = $p->{profile};
94
    if ($p->{row_ident} eq $self->_order_column) {
95
      # no need to handle
96
      delete @{$prof}{qw(oreqnumber)};
97
    }
98
    if ($p->{row_ident} eq $self->_item_column) {
99
      # no need to handle
100
      delete @{$prof}{qw(delivery_order_id)};
101
    }
102
    if ($p->{row_ident} eq $self->_stock_column) {
103
      # no need to handle
104
      delete @{$prof}{qw(delivery_order_item_id)};
105
      delete @{$prof}{qw(bestbefore)} if !$::instance_conf->get_show_bestbefore;
106
    }
107
  }
108

  
109
  return $profile;
110
}
111

  
112
sub init_existing_objects {
113
  my ($self) = @_;
114

  
115
  # only use objects of main class (the first one)
116
  eval "require " . $self->class->[0];
117
  $self->existing_objects($self->manager_class->[0]->get_all);
118
}
119

  
120
sub get_duplicate_check_fields {
121
  return {
122
    donumber => {
123
      label     => $::locale->text('Delivery Order Number'),
124
      default   => 1,
125
      std_check => 1,
126
      maker     => sub {
127
        my ($object, $worker) = @_;
128
        return if ref $object ne $worker->class->[0];
129
        return $object->donumber;
130
      },
131
    },
132
  };
133
}
134

  
135
sub check_std_duplicates {
136
  my $self = shift;
137

  
138
  my $duplicates = {};
139

  
140
  my $all_fields = $self->get_duplicate_check_fields();
141

  
142
  foreach my $key (keys(%{ $all_fields })) {
143
    if ( $self->controller->profile->get('duplicates_'. $key) && (!exists($all_fields->{$key}->{std_check}) || $all_fields->{$key}->{std_check} )  ) {
144
      $duplicates->{$key} = {};
145
    }
146
  }
147

  
148
  my @duplicates_keys = keys(%{ $duplicates });
149

  
150
  if ( !scalar(@duplicates_keys) ) {
151
    return;
152
  }
153

  
154
  if ( $self->controller->profile->get('duplicates') eq 'check_db' ) {
155
    foreach my $object (@{ $self->existing_objects }) {
156
      foreach my $key (@duplicates_keys) {
157
        my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
158
        $duplicates->{$key}->{$value} = 'db';
159
      }
160
    }
161
  }
162

  
163
  # only check order rows
164
  foreach my $entry (@{ $self->controller->data }) {
165
    if ($entry->{raw_data}->{datatype} ne $self->_order_column) {
166
      next;
167
    }
168
    if ( @{ $entry->{errors} } ) {
169
      next;
170
    }
171

  
172
    my $object = $entry->{object};
173

  
174
    foreach my $key (@duplicates_keys) {
175
      my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
176

  
177
      if ( exists($duplicates->{$key}->{$value}) ) {
178
        push(@{ $entry->{errors} }, $duplicates->{$key}->{$value} eq 'db' ? $::locale->text('Duplicate in database') : $::locale->text('Duplicate in CSV file'));
179
        last;
180
      } else {
181
        $duplicates->{$key}->{$value} = 'csv';
182
      }
183

  
184
    }
185
  }
186
}
187

  
188
sub setup_displayable_columns {
189
  my ($self) = @_;
190

  
191
  $self->SUPER::setup_displayable_columns;
192

  
193
  $self->add_cvar_columns_to_displayable_columns($self->_order_column);
194

  
195
  $self->add_displayable_columns($self->_order_column,
196
                                 { name => 'datatype',                description => $self->_order_column . ' [1]'                            },
197
                                 { name => 'closed',                  description => $::locale->text('Closed')                                },
198
                                 { name => 'contact',                 description => $::locale->text('Contact Person (name)')                 },
199
                                 { name => 'cp_id',                   description => $::locale->text('Contact Person (database ID)')          },
200
                                 { name => 'currency',                description => $::locale->text('Currency')                              },
201
                                 { name => 'currency_id',             description => $::locale->text('Currency (database ID)')                },
202
                                 { name => 'customer',                description => $::locale->text('Customer (name)')                       },
203
                                 { name => 'customernumber',          description => $::locale->text('Customer Number')                       },
204
                                 { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
205
                                 { name => 'cusordnumber',            description => $::locale->text('Customer Order Number')                 },
206
                                 { name => 'delivered',               description => $::locale->text('Delivered')                             },
207
                                 { name => 'delivery_term',           description => $::locale->text('Delivery terms (name)')                 },
208
                                 { name => 'delivery_term_id',        description => $::locale->text('Delivery terms (database ID)')          },
209
                                 { name => 'department_id',           description => $::locale->text('Department (database ID)')              },
210
                                 { name => 'department',              description => $::locale->text('Department (description)')              },
211
                                 { name => 'donumber',                description => $::locale->text('Delivery Order Number')                 },
212
                                 { name => 'employee_id',             description => $::locale->text('Employee (database ID)')                },
213
                                 { name => 'globalproject',           description => $::locale->text('Document Project (description)')        },
214
                                 { name => 'globalprojectnumber',     description => $::locale->text('Document Project (number)')             },
215
                                 { name => 'globalproject_id',        description => $::locale->text('Document Project (database ID)')        },
216
                                 { name => 'intnotes',                description => $::locale->text('Internal Notes')                        },
217
                                 { name => 'is_sales',                description => $::locale->text('Is sales')                              },
218
                                 { name => 'language',                description => $::locale->text('Language (name)')                       },
219
                                 { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
220
                                 { name => 'notes',                   description => $::locale->text('Notes')                                 },
221
                                 { name => 'ordnumber',               description => $::locale->text('Order Number')                          },
222
                                 { name => 'payment',                 description => $::locale->text('Payment terms (name)')                  },
223
                                 { name => 'payment_id',              description => $::locale->text('Payment terms (database ID)')           },
224
                                 { name => 'reqdate',                 description => $::locale->text('Reqdate')                               },
225
                                 { name => 'salesman_id',             description => $::locale->text('Salesman (database ID)')                },
226
                                 { name => 'shippingpoint',           description => $::locale->text('Shipping Point')                        },
227
                                 { name => 'shipvia',                 description => $::locale->text('Ship via')                              },
228
                                 { name => 'shipto_id',               description => $::locale->text('Ship to (database ID)')                 },
229
                                 { name => 'taxincluded',             description => $::locale->text('Tax Included')                          },
230
                                 { name => 'taxzone',                 description => $::locale->text('Tax zone (description)')                },
231
                                 { name => 'taxzone_id',              description => $::locale->text('Tax zone (database ID)')                },
232
                                 { name => 'transaction_description', description => $::locale->text('Transaction description')               },
233
                                 { name => 'transdate',               description => $::locale->text('Order Date')                            },
234
                                 { name => 'vendor',                  description => $::locale->text('Vendor (name)')                         },
235
                                 { name => 'vendornumber',            description => $::locale->text('Vendor Number')                         },
236
                                 { name => 'vendor_id',               description => $::locale->text('Vendor (database ID)')                  },
237
                                );
238

  
239
  $self->add_cvar_columns_to_displayable_columns($self->_item_column);
240

  
241
  $self->add_displayable_columns($self->_item_column,
242
                                 { name => 'datatype',        description => $self->_item_column . ' [1]'                  },
243
                                 { name => 'cusordnumber',    description => $::locale->text('Customer Order Number')      },
244
                                 { name => 'description',     description => $::locale->text('Description')                },
245
                                 { name => 'discount',        description => $::locale->text('Discount')                   },
246
                                 { name => 'lastcost',        description => $::locale->text('Lastcost')                   },
247
                                 { name => 'longdescription', description => $::locale->text('Long Description')           },
248
                                 { name => 'ordnumber',       description => $::locale->text('Order Number')               },
249
                                 { name => 'partnumber',      description => $::locale->text('Part Number')                },
250
                                 { name => 'parts_id',        description => $::locale->text('Part (database ID)')         },
251
                                 { name => 'position',        description => $::locale->text('position')                   },
252
                                 { name => 'price_factor',    description => $::locale->text('Price factor (name)')        },
253
                                 { name => 'price_factor_id', description => $::locale->text('Price factor (database ID)') },
254
                                 { name => 'pricegroup',      description => $::locale->text('Price group (name)')         },
255
                                 { name => 'pricegroup_id',   description => $::locale->text('Price group (database ID)')  },
256
                                 { name => 'project',         description => $::locale->text('Project (description)')      },
257
                                 { name => 'projectnumber',   description => $::locale->text('Project (number)')           },
258
                                 { name => 'project_id',      description => $::locale->text('Project (database ID)')      },
259
                                 { name => 'qty',             description => $::locale->text('Quantity')                   },
260
                                 { name => 'reqdate',         description => $::locale->text('Reqdate')                    },
261
                                 { name => 'sellprice',       description => $::locale->text('Sellprice')                  },
262
                                 { name => 'serialnumber',    description => $::locale->text('Serial No.')                 },
263
                                 { name => 'transdate',       description => $::locale->text('Order Date')                 },
264
                                 { name => 'unit',            description => $::locale->text('Unit')                       },
265
                                );
266

  
267
  $self->add_cvar_columns_to_displayable_columns($self->_stock_column);
268

  
269
  $self->add_displayable_columns($self->_stock_column,
270
                                 { name => 'datatype',     description => $self->_stock_column . ' [1]'              },
271
                                 { name => 'warehouse',    description => $::locale->text('Warehouse')               },
272
                                 { name => 'warehouse_id', description => $::locale->text('Warehouse (database ID)') },
273
                                 { name => 'bin',          description => $::locale->text('Bin')                     },
274
                                 { name => 'bin_id',       description => $::locale->text('Bin (database ID)')       },
275
                                 { name => 'chargenumber', description => $::locale->text('Charge number')           },
276
                                 { name => 'qty',          description => $::locale->text('Quantity')                },
277
                                 { name => 'unit',         description => $::locale->text('Unit')                    },
278
                                );
279
  if ($::instance_conf->get_show_bestbefore) {
280
    $self->add_displayable_columns($self->_stock_column,
281
                                   { name => 'bestbefore', description => $::locale->text('Best Before') });
282
  }
283
}
284

  
285

  
286
sub init_languages_by {
287
  my ($self) = @_;
288

  
289
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $self->all_languages } } ) } qw(id description article_code) };
290
}
291

  
292
sub init_all_parts {
293
  my ($self) = @_;
294

  
295
  return SL::DB::Manager::Part->get_all(where => [or => [ obsolete => 0, obsolete => undef ]]);
296
}
297

  
298
sub init_parts_by {
299
  my ($self) = @_;
300

  
301
  return { map { my $col = $_; ( $col => { map { ( trim($_->$col) => $_ ) } @{ $self->all_parts } } ) } qw(id partnumber ean description) };
302
}
303

  
304
sub init_part_counts_by {
305
  my ($self) = @_;
306

  
307
  my $part_counts_by;
308

  
309
  $part_counts_by->{ean}->        {trim($_->ean)}++         for @{ $self->all_parts };
310
  $part_counts_by->{description}->{trim($_->description)}++ for @{ $self->all_parts };
311

  
312
  return $part_counts_by;
313
}
314

  
315
sub init_contacts_by {
316
  my ($self) = @_;
317

  
318
  my $all_contacts = SL::DB::Manager::Contact->get_all;
319

  
320
  my $cby;
321
  # by customer/vendor id  _and_  contact person id
322
  $cby->{'cp_cv_id+cp_id'}   = { map { ( $_->cp_cv_id . '+' . $_->cp_id   => $_ ) } @{ $all_contacts } };
323
  # by customer/vendor id  _and_  contact person name
324
  $cby->{'cp_cv_id+cp_name'} = { map { ( $_->cp_cv_id . '+' . $_->cp_name => $_ ) } @{ $all_contacts } };
325

  
326
  return $cby;
327
}
328

  
329
sub init_ct_shiptos_by {
330
  my ($self) = @_;
331

  
332
  my $all_ct_shiptos = SL::DB::Manager::Shipto->get_all(query => [module => 'CT']);
333

  
334
  my $sby;
335
  # by trans_id  _and_  shipto_id
336
  $sby->{'trans_id+shipto_id'} = { map { ( $_->trans_id . '+' . $_->shipto_id => $_ ) } @{ $all_ct_shiptos } };
337

  
338
  return $sby;
339
}
340

  
341
sub init_price_factors_by {
342
  my ($self) = @_;
343

  
344
  my $all_price_factors = SL::DB::Manager::PriceFactor->get_all;
345
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_price_factors } } ) } qw(id description) };
346
}
347

  
348
sub init_pricegroups_by {
349
  my ($self) = @_;
350

  
351
  my $all_pricegroups = SL::DB::Manager::Pricegroup->get_all;
352
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_pricegroups } } ) } qw(id pricegroup) };
353
}
354

  
355
sub init_units_by {
356
  my ($self) = @_;
357

  
358
  my $all_units = SL::DB::Manager::Unit->get_all;
359
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_units } } ) } qw(name) };
360
}
361

  
362
sub init_warehouses_by {
363
  my ($self) = @_;
364

  
365
  my $all_warehouses = SL::DB::Manager::Warehouse->get_all(query => [ or => [ invalid => 0, invalid => undef ]]);
366
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_warehouses } } ) } qw(id description) };
367
}
368

  
369
sub init_bins_by {
370
  my ($self) = @_;
371

  
372
  my $all_bins = SL::DB::Manager::Bin->get_all();
373
  my $bins_by;
374
  $bins_by->{_wh_id_and_id_ident()}          = { map { ( _wh_id_and_id_maker($_->warehouse_id, $_->id)                   => $_ ) } @{ $all_bins } };
375
  $bins_by->{_wh_id_and_description_ident()} = { map { ( _wh_id_and_description_maker($_->warehouse_id, $_->description) => $_ ) } @{ $all_bins } };
376

  
377
  return $bins_by;
378
}
379

  
380
sub init_transfer_types_by {
381
  my ($self) = @_;
382

  
383
  my $all_transfer_types = SL::DB::Manager::TransferType->get_all();
384
  my $transfer_types_by;
385
  $transfer_types_by->{_transfer_type_dir_and_description_ident()} = {
386
    map { ( _transfer_type_dir_and_description_maker($_->direction, $_->description) => $_ ) } @{ $all_transfer_types }
387
  };
388

  
389
  return $transfer_types_by;
390
}
391

  
392
sub check_objects {
393
  my ($self) = @_;
394

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

  
397
  my $i = 0;
398
  my $num_data = scalar @{ $self->controller->data };
399
  my $order_entry;
400
  my $item_entry;
401
  foreach my $entry (@{ $self->controller->data }) {
402
    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
403

  
404
    $entry->{info_data}->{datatype} = $entry->{raw_data}->{datatype};
405

  
406
    if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
407
      $self->handle_order($entry);
408
      $order_entry = $entry;
409
    } elsif ($entry->{raw_data}->{datatype} eq $self->_item_column && $entry->{object}->can('part')) {
410
      $self->handle_item($entry, $order_entry);
411
      $item_entry = $entry;
412
    } elsif ($entry->{raw_data}->{datatype} eq $self->_stock_column) {
413
      $self->handle_stock($entry, $item_entry, $order_entry);
414
      push @{ $order_entry->{errors} }, $::locale->text('Error: Stock problem') if scalar(@{$entry->{errors}}) > 0;
415
    } else {
416
      $order_entry = undef;
417
      $item_entry  = undef;
418
    }
419

  
420
    $self->handle_cvars($entry, sub_module => 'delivery_order_items');
421

  
422
  } continue {
423
    $i++;
424
  }
425

  
426
  $self->add_info_columns($self->_order_column,
427
                          { header => $::locale->text('Data type'), method => 'datatype' });
428
  $self->add_info_columns($self->_item_column,
429
                          { header => $::locale->text('Data type'), method => 'datatype' });
430
  $self->add_info_columns($self->_stock_column,
431
                          { header => $::locale->text('Data type'), method => 'datatype' });
432

  
433
  $self->add_info_columns($self->_order_column,
434
                          { header => $::locale->text('Customer/Vendor'), method => 'vc_name' });
435
  # Todo: access via ->[0] ok? Better: search first order column and use this
436
  $self->add_columns($self->_order_column,
437
                     map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment delivery_term language department globalproject taxzone cp currency));
438
  $self->add_columns($self->_order_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
439
  $self->add_columns($self->_order_column, 'cp_id')            if exists $self->controller->data->[0]->{raw_data}->{contact};
440

  
441
  $self->add_info_columns($self->_item_column,
442
                          { header => $::locale->text('Part Number'), method => 'partnumber' });
443
  # Todo: access via ->[1] ok? Better: search first item column and use this
444
  $self->add_columns($self->_item_column,
445
                     map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project price_factor pricegroup));
446
  $self->add_columns($self->_item_column, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
447

  
448
  $self->add_cvar_raw_data_columns();
449

  
450

  
451
  # Check overall qtys for sales delivery orders, because they are
452
  # stocked out in the end and a stock underrun can occure.
453
  # Todo: let it work even with bestbefore turned off.
454
  $order_entry = undef;
455
  $item_entry  = undef;
456
  my %wanted_qtys_by_part_wh_bin_charge_bestbefore;
457
  my %stock_entries_with_part_wh_bin_charge_bestbefore;
458
  my %order_entries_with_part_wh_bin_charge_bestbefore;
459
  foreach my $entry (@{ $self->controller->data }) {
460
    if ($entry->{raw_data}->{datatype} eq $self->_order_column) {
461
      if (scalar(@{ $entry->{errors} }) || !$entry->{object}->is_sales) {
462
        $order_entry = undef;
463
        $item_entry  = undef;
464
        next;
465
      }
466
      $order_entry = $entry;
467

  
468
    } elsif (defined $order_entry && $entry->{raw_data}->{datatype} eq $self->_item_column) {
469
      if (scalar(@{ $entry->{errors} })) {
470
        $item_entry = undef;
471
        next;
472
      }
473
      $item_entry = $entry;
474

  
475
    } elsif (defined $item_entry && $entry->{raw_data}->{datatype} eq $self->_stock_column) {
476
      my $object = $entry->{object};
477
      my $key = join('+',
478
                     $item_entry->{object}->parts_id,
479
                     $object->warehouse_id,
480
                     $object->bin_id,
481
                     $object->chargenumber,
482
                     $object->bestbefore);
483
      $wanted_qtys_by_part_wh_bin_charge_bestbefore{$key} += $object->qty;
484
      push @{$order_entries_with_part_wh_bin_charge_bestbefore{$key}}, $order_entry;
485
      push @{$stock_entries_with_part_wh_bin_charge_bestbefore{$key}}, $entry;
486
    }
487
  }
488

  
489
  foreach my $key (keys %wanted_qtys_by_part_wh_bin_charge_bestbefore) {
490
    my ($parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore) = split '\+', $key;
491
    my $qty = $self->get_stocked_qty($parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore);
492
    if ($wanted_qtys_by_part_wh_bin_charge_bestbefore{$key} > $qty) {
493

  
494
      foreach my $stock_entry (@{ $stock_entries_with_part_wh_bin_charge_bestbefore{$key} }) {
495
        push @{ $stock_entry->{errors} }, $::locale->text('Error: Stocking out would result in stock underrun');
496
      }
497

  
498
      foreach my $order_entry (uniq @{ $order_entries_with_part_wh_bin_charge_bestbefore{$key} }) {
499
        my $part            = $self->parts_by->{id}->{$parts_id}->displayable_name;
500
        my $stock           = $self->bins_by->{_wh_id_and_id_ident()}->{_wh_id_and_id_maker($wh_id, $bin_id)}->full_description;
501
        my $bestbefore_obj  = $::locale->parse_date_to_object($bestbefore, dateformat=>'yyyy-mm-dd');
502
        my $bestbefore_text = $bestbefore_obj? $::locale->parse_date_to_object($bestbefore_obj, dateformat=>'yyyy-mm-dd')->to_kivitendo: '-';
503
        my $wanted_qty      = $wanted_qtys_by_part_wh_bin_charge_bestbefore{$key};
504
        my $details_text    = sprintf('%s (%s / %s / %s): %s > %s',
505
                                      $part,
506
                                      $stock,
507
                                      $chargenumber,
508
                                      $bestbefore_text,
509
                                      $::form->format_amount(\%::myconfig, $wanted_qty,  2),
510
                                      $::form->format_amount(\%::myconfig, $qty, 2));
511
        push @{ $order_entry->{errors} }, $::locale->text('Error: Stocking out would result in stock underrun: #1', $details_text);
512
      }
513

  
514
    }
515
  }
516

  
517
}
518

  
519
sub handle_order {
520
  my ($self, $entry) = @_;
521

  
522
  my $object = $entry->{object};
523

  
524
  $object->orderitems([]);
525

  
526
  $self->handle_order_sources($entry);
527
  my $first_source_order = $object->{source_orders}->[0];
528

  
529
  my $vc_obj;
530
  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
531
    $self->check_vc($entry, 'customer_id');
532
    $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
533

  
534
  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
535
    $self->check_vc($entry, 'vendor_id');
536
    $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
537

  
538
  } else {
539
    # customer / vendor from (first) source order if not given
540
    if ($first_source_order) {
541
      if ($first_source_order->customer) {
542
        $vc_obj = $first_source_order->customer;
543
        $object->customer($first_source_order->customer);
544
      } elsif ($first_source_order->vendor) {
545
        $vc_obj = $first_source_order->vendor;
546
        $object->vendor($first_source_order->vendor);
547
      }
548
    }
549
  }
550

  
551
  if (!$vc_obj) {
552
    push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
553
  }
554

  
555
  $self->handle_is_sales($entry);
556
  $self->check_contact($entry);
557
  $self->check_language($entry);
558
  $self->check_payment($entry);
559
  $self->check_delivery_term($entry);
560
  $self->check_department($entry);
561
  $self->check_project($entry, global => 1);
562
  $self->check_ct_shipto($entry);
563
  $self->check_taxzone($entry);
564
  $self->check_currency($entry, take_default => 0);
565

  
566
  # copy from (first) source order if not given
567
  # if no source order, then copy some values from customer/vendor
568
  if ($first_source_order) {
569
    foreach (qw(cusordnumber notes intnotes shippingpoint shipvia
570
                transaction_description currency_id delivery_term_id
571
                department_id language_id payment_id globalproject_id shipto_id
572
                taxzone_id)) {
573
      $object->$_($first_source_order->$_) unless $object->$_;
574
    }
575
  } elsif ($vc_obj) {
576
    foreach (qw(currency_id delivery_term_id language_id payment_id taxzone_id)) {
577
      $object->$_($vc_obj->$_) unless $object->$_;
578
    }
579
    $object->intnotes($vc_obj->notes) unless $object->intnotes;
580
  }
581

  
582
  $self->handle_salesman($entry);
583
  $self->handle_employee($entry);
584
}
585

  
586
sub handle_item {
587
  my ($self, $entry, $order_entry) = @_;
588

  
589
  return unless $order_entry;
590

  
591
  my $order_obj = $order_entry->{object};
592
  my $object    = $entry->{object};
593
  $object->delivery_order_stock_entries([]);
594

  
595
  if (!$self->check_part($entry)) {
596
    if ($self->controller->profile->get('ignore_faulty_positions')) {
597
      push @{ $order_entry->{information} }, $::locale->text('Warning: Faulty position ignored');
598
    } else {
599
      push @{ $order_entry->{errors} }, $::locale->text('Error: Faulty position in this delivery order');
600
    }
601
    return;
602
  }
603

  
604
  $order_obj->add_items($object);
605

  
606
  my $part_obj = SL::DB::Part->new(id => $object->parts_id)->load;
607

  
608
  $self->handle_item_source($entry, $order_entry);
609
  $object->position($object->{source_item}->position) if $object->{source_item};
610

  
611
  $self->handle_unit($entry);
612

  
613
  # copy from part if not given
614
  $object->description($part_obj->description) unless $object->description;
615
  $object->longdescription($part_obj->notes)   unless $object->longdescription;
616
  $object->lastcost($part_obj->lastcost)       unless defined $object->lastcost;
617

  
618
  $self->check_project($entry, global => 0);
619
  $self->check_price_factor($entry);
620
  $self->check_pricegroup($entry);
621

  
622
  $self->handle_sellprice($entry, $order_entry);
623
  $self->handle_discount($entry, $order_entry);
624

  
625
  push @{ $order_entry->{errors} }, $::locale->text('Error: Faulty position in this delivery order') if scalar(@{$entry->{errors}}) > 0;
626
}
627

  
628
sub handle_stock {
629
  my ($self, $entry, $item_entry, $order_entry) = @_;
630

  
631
  return unless $item_entry;
632

  
633
  my $item_obj  = $item_entry->{object};
634
  return unless $item_obj->part;
635

  
636
  my $order_obj = $order_entry->{object};
637
  my $object    = $entry->{object};
638

  
639
  $item_obj->add_delivery_order_stock_entries($object);
640

  
641
  $self->check_warehouse($entry);
642
  $self->check_bin($entry);
643

  
644
  $self->handle_unit($entry, $item_obj->part);
645

  
646
  # check if enough is stocked
647
  # not necessary, because overall stock underrun is checked later
648
  # if ($order_obj->is_sales) {
649
  #   my $stocked_qty = $self->get_stocked_qty($item_obj->parts_id,
650
  #                                            $object->warehouse_id,
651
  #                                            $object->bin_id,
652
  #                                            $object->chargenumber,
653
  #                                            $object->bestbefore);
654
  #   if ($stocked_qty < $object->qty) {
655
  #     push @{ $entry->{errors} }, $::locale->text('Error: Not enough parts in stock');
656
  #   }
657
  # }
658

  
659
  my ($stock_info_entry, $part) = @_;
660

  
661
  # Todo: option: should stock?
662
  if (1) {
663
    my $tt_key = $order_obj->is_sales
664
               ? _transfer_type_dir_and_description_maker('out', 'shipped')
665
               : _transfer_type_dir_and_description_maker('in', 'stock');
666
    my $trans_type_id = $self->transfer_types_by->{_transfer_type_dir_and_description_ident()}{$tt_key}->id;
667

  
668
    my $qty = $order_obj->is_sales ? -1*($object->qty) : $object->qty;
669
    my $inventory = SL::DB::Inventory->new(
670
      parts_id      => $item_obj->parts_id,
671
      warehouse_id  => $object->warehouse_id,
672
      bin_id        => $object->bin_id,
673
      trans_type_id => $trans_type_id,
674
      qty           => $qty,
675
      chargenumber  => $object->chargenumber,
676
      employee_id   => $order_obj->employee_id,
677
      shippingdate  => ($order_obj->reqdate || DateTime->today_local),
678
      comment       => $order_obj->transaction_description,
679
      project_id    => ($order_obj->globalproject_id || $item_obj->project_id),
680
    );
681
    $inventory->bestbefore($object->bestbefore) if $::instance_conf->get_show_bestbefore;
682
    $object->{inventory_obj} = $inventory;
683
    $order_obj->delivered(1);
684
  }
685
}
686

  
687
sub handle_is_sales {
688
  my ($self, $entry) = @_;
689

  
690
  if (!exists $entry->{raw_data}->{is_sales}) {
691
    $entry->{object}->is_sales(!!$entry->{object}->customer_id);
692
  }
693
}
694

  
695
sub handle_order_sources {
696
  my ($self, $entry) = @_;
697

  
698
  my $record = $entry->{object};
699

  
700
  $record->{source_orders} = [];
701
  return $record->{source_orders} if !$record->ordnumber;
702

  
703
  my @order_numbers = split ' ', $record->ordnumber;
704

  
705
  my $orders = SL::DB::Manager::Order->get_all(where => [ordnumber => \@order_numbers]);
706

  
707
  if (scalar @$orders == 0) {
708
    push @{ $entry->{errors} }, $::locale->text('Error: Source order not found');
709
  } elsif (scalar @$orders > 1) {
710
    push @{ $entry->{errors} }, $::locale->text('Error: More than one source order found');
711
  }
712

  
713
  $record->{source_orders} = $orders;
714
}
715

  
716
sub handle_item_source {
717
  my ($self, $entry, $record_entry) = @_;
718

  
719
  my $item   = $entry->{object};
720
  my $record = $record_entry->{object};
721

  
722
  return if !@{ $record->{source_orders} };
723

  
724
  foreach my $order (@{ $record->{source_orders} }) {
725
    $item->{source_item} = first { $item->parts_id == $_->parts_id && $item->qty == $_->qty} @{ $order->items_sorted };
726
    last if $item->{source_item};
727
  }
728
}
729

  
730
sub handle_unit {
731
  my ($self, $entry, $part) = @_;
732

  
733
  my $object = $entry->{object};
734

  
735
  $part ||= $object->part;
736

  
737
  # Set unit from part if not given.
738
  if (!$object->unit) {
739
    $object->unit($part->unit);
740
    return 1;
741
  }
742

  
743
  # Check whether or not unit is valid.
744
  if ($object->unit && !$self->units_by->{name}->{ $object->unit }) {
745
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
746
    return 0;
747
  }
748

  
749
  # Check whether unit is convertible to parts unit
750
  if (none { $object->unit eq $_ } map { $_->name } @{ $part->unit_obj->convertible_units }) {
751
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid unit');
752
    return 0;
753
  }
754

  
755
  return 1;
756
}
757

  
758
sub handle_sellprice {
759
  my ($self, $entry, $record_entry) = @_;
760

  
761
  my $item   = $entry->{object};
762
  my $record = $record_entry->{object};
763

  
764
  return if !$record->customervendor;
765

  
766
  # If sellprice is given, set price source to pricegroup if given or to none.
767
  if (exists $entry->{raw_data}->{sellprice}) {
768
    my $price_source      = SL::PriceSource->new(record_item => $item, record => $record);
769
    my $price_source_spec = $item->pricegroup_id ? 'pricegroup' . '/' . $item->pricegroup_id : '';
770
    my $price             = $price_source->price_from_source($price_source_spec);
771
    $item->active_price_source($price->source);
772

  
773
  } else {
774

  
775
    if ($item->{source_item}) {
776
      # Set sellprice from source order item if not given. Convert with respect to unit.
777
      my $sellprice = $item->{source_item}->sellprice;
778
      if ($item->unit ne $item->{source_item}->unit) {
779
        $sellprice = $item->unit_obj->convert_to($sellprice, $item->{source_item}->unit_obj);
780
      }
781
      $item->sellprice($sellprice);
782
      $item->active_price_source($item->{source_item}->active_price_source);
783

  
784
    } else {
785
      # Set sellprice the best price of price source
786
      my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
787
      my $price        = $price_source->best_price;
788
      if ($price) {
789
        $item->sellprice($price->price);
790
        $item->active_price_source($price->source);
791
      } else {
792
        $item->sellprice(0);
793
        $item->active_price_source($price_source->price_from_source('')->source);
794
      }
795
    }
796
  }
797
}
798

  
799
sub handle_discount {
800
  my ($self, $entry, $record_entry) = @_;
801

  
802
  my $item   = $entry->{object};
803
  my $record = $record_entry->{object};
804

  
805
  return if !$record->customervendor;
806

  
807
  # If discount is given, set discount to none.
808
  if (exists $entry->{raw_data}->{discount}) {
809
    my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
810
    my $discount     = $price_source->price_from_source('');
811
    $item->active_discount_source($discount->source);
812

  
813
  } else {
814

  
815
    if ($item->{source_item}) {
816
      # Set discount from source order item if not given.
817
      $item->discount($item->{source_item}->discount);
818
      $item->active_discount_source($item->{source_item}->active_discount_source);
819

  
820
    } else {
821
      # Set discount the best discount of price source
822
      my $price_source = SL::PriceSource->new(record_item => $item, record => $record);
823
      my $discount     = $price_source->best_discount;
824
      if ($discount) {
825
        $item->discount($discount->discount);
826
        $item->active_discount_source($discount->source);
827
      } else {
828
        $item->discount(0);
829
        $item->active_discount_source($price_source->discount_from_source('')->source);
830
      }
831
    }
832
  }
833
}
834

  
835
sub check_contact {
836
  my ($self, $entry) = @_;
837

  
838
  my $object = $entry->{object};
839

  
840
  my $cp_cv_id = $object->customer_id || $object->vendor_id;
841
  return 0 unless $cp_cv_id;
842

  
843
  # Check whether or not contact ID is valid.
844
  if ($object->cp_id && !$self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }) {
845
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
846
    return 0;
847
  }
848

  
849
  # Map name to ID if given.
850
  if (!$object->cp_id && $entry->{raw_data}->{contact}) {
851
    my $cp = $self->contacts_by->{'cp_cv_id+cp_name'}->{ $cp_cv_id . '+' . $entry->{raw_data}->{contact} };
852
    if (!$cp) {
853
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid contact');
854
      return 0;
855
    }
856

  
857
    $object->cp_id($cp->cp_id);
858
  }
859

  
860
  if ($object->cp_id) {
861
    $entry->{info_data}->{contact} = $self->contacts_by->{'cp_cv_id+cp_id'}->{ $cp_cv_id . '+' . $object->cp_id }->cp_name;
862
  }
863

  
864
  return 1;
865
}
866

  
867
sub check_language {
868
  my ($self, $entry) = @_;
869

  
870
  my $object = $entry->{object};
871

  
872
  # Check whether or not language ID is valid.
873
  if ($object->language_id && !$self->languages_by->{id}->{ $object->language_id }) {
874
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
875
    return 0;
876
  }
877

  
878
  # Map name to ID if given.
879
  if (!$object->language_id && $entry->{raw_data}->{language}) {
880
    my $language = $self->languages_by->{description}->{  $entry->{raw_data}->{language} }
881
                || $self->languages_by->{article_code}->{ $entry->{raw_data}->{language} };
882

  
883
    if (!$language) {
884
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid language');
885
      return 0;
886
    }
887

  
888
    $object->language_id($language->id);
889
  }
890

  
891
  if ($object->language_id) {
892
    $entry->{info_data}->{language} = $self->languages_by->{id}->{ $object->language_id }->description;
893
  }
894

  
895
  return 1;
896
}
897

  
898
sub check_ct_shipto {
899
  my ($self, $entry) = @_;
900

  
901
  my $object = $entry->{object};
902

  
903
  my $trans_id = $object->customer_id || $object->vendor_id;
904
  return 0 unless $trans_id;
905

  
906
  # Check whether or not shipto ID is valid.
907
  if ($object->shipto_id && !$self->ct_shiptos_by->{'trans_id+shipto_id'}->{ $trans_id . '+' . $object->shipto_id }) {
908
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid shipto');
909
    return 0;
910
  }
911

  
912
  return 1;
913
}
914

  
915
sub check_part {
916
  my ($self, $entry) = @_;
917

  
918
  my $object = $entry->{object};
919
  my $is_ambiguous;
920

  
921
  # Check whether or not part ID is valid.
922
  if ($object->parts_id && !$self->parts_by->{id}->{ $object->parts_id }) {
923
    push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
924
    return 0;
925
  }
926

  
927
  # Map number to ID if given.
928
  if (!$object->parts_id && $entry->{raw_data}->{partnumber}) {
929
    my $part = $self->parts_by->{partnumber}->{ trim($entry->{raw_data}->{partnumber}) };
930
    if (!$part) {
931
      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
932
      return 0;
933
    }
934

  
935
    $object->parts_id($part->id);
936
  }
937

  
938
  # Map description to ID if given.
939
  if (!$object->parts_id && $entry->{raw_data}->{description}) {
940
    my $part = $self->parts_by->{description}->{ trim($entry->{raw_data}->{description}) };
941
    if (!$part) {
942
      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
943
      return 0;
944
    }
945

  
946
    if ($self->part_counts_by->{description}->{ trim($entry->{raw_data}->{description}) } > 1) {
947
      $is_ambiguous = 1;
948
    } else {
949
      $object->parts_id($part->id);
950
    }
951
  }
952

  
953
  # Map ean to ID if given.
954
  if (!$object->parts_id && $entry->{raw_data}->{ean}) {
955
    my $part = $self->parts_by->{ean}->{ trim($entry->{raw_data}->{ean}) };
956
    if (!$part) {
957
      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
958
      return 0;
959
    }
960

  
961
    if ($self->part_counts_by->{ean}->{ trim($entry->{raw_data}->{ean}) } > 1) {
962
      $is_ambiguous = 1;
963
    } else {
964
      $object->parts_id($part->id);
965
    }
966
  }
967

  
968
  if ($object->parts_id) {
969
    $entry->{info_data}->{partnumber} = $self->parts_by->{id}->{ $object->parts_id }->partnumber;
970
  } else {
971
    if ($is_ambiguous) {
972
      push @{ $entry->{errors} }, $::locale->text('Error: Part is ambiguous');
973
    } else {
974
      push @{ $entry->{errors} }, $::locale->text('Error: Part not found');
975
    }
976
    return 0;
977
  }
978

  
979
  return 1;
980
}
981

  
982
sub check_price_factor {
983
  my ($self, $entry) = @_;
984

  
985
  my $object = $entry->{object};
986

  
987
  # Check whether or not price_factor ID is valid.
988
  if ($object->price_factor_id && !$self->price_factors_by->{id}->{ $object->price_factor_id }) {
989
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
990
    return 0;
991
  }
992

  
993
  # Map description to ID if given.
994
  if (!$object->price_factor_id && $entry->{raw_data}->{price_factor}) {
995
    my $price_factor = $self->price_factors_by->{description}->{ $entry->{raw_data}->{price_factor} };
996
    if (!$price_factor) {
997
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid price factor');
998
      return 0;
999
    }
1000

  
1001
    $object->price_factor_id($price_factor->id);
1002
  }
1003

  
1004
  return 1;
1005
}
1006

  
1007
sub check_pricegroup {
1008
  my ($self, $entry) = @_;
1009

  
1010
  my $object = $entry->{object};
1011

  
1012
  # Check whether or not pricegroup ID is valid.
1013
  if ($object->pricegroup_id && !$self->pricegroups_by->{id}->{ $object->pricegroup_id }) {
1014
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid price group');
1015
    return 0;
1016
  }
1017

  
1018
  # Map pricegroup to ID if given.
1019
  if (!$object->pricegroup_id && $entry->{raw_data}->{pricegroup}) {
1020
    my $pricegroup = $self->pricegroups_by->{pricegroup}->{ $entry->{raw_data}->{pricegroup} };
1021
    if (!$pricegroup) {
1022
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid price group');
1023
      return 0;
1024
    }
1025

  
1026
    $object->pricegroup_id($pricegroup->id);
1027
  }
1028

  
1029
  return 1;
1030
}
1031

  
1032
sub check_warehouse {
1033
  my ($self, $entry) = @_;
1034

  
1035
  my $object = $entry->{object};
1036

  
1037
  # Check whether or not warehouse ID is valid.
1038
  if ($object->warehouse_id && !$self->warehouses_by->{id}->{ $object->warehouse_id }) {
1039
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
1040
    return 0;
1041
  }
1042

  
1043
  # Map description to ID if given.
1044
  if (!$object->warehouse_id && $entry->{raw_data}->{warehouse}) {
1045
    my $wh = $self->warehouses_by->{description}->{ $entry->{raw_data}->{warehouse} };
1046
    if (!$wh) {
1047
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid warehouse');
1048
      return 0;
1049
    }
1050

  
1051
    $object->warehouse_id($wh->id);
1052
  }
1053

  
1054
  if ($object->warehouse_id) {
1055
    $entry->{info_data}->{warehouse} = $self->warehouses_by->{id}->{ $object->warehouse_id }->description;
1056
  } else {
1057
    push @{ $entry->{errors} }, $::locale->text('Error: Warehouse not found');
1058
    return 0;
1059
  }
1060

  
1061
  return 1;
1062
}
1063

  
1064
# Check bin for given warehouse, so check_warehouse must be called first.
1065
sub check_bin {
1066
  my ($self, $entry) = @_;
1067

  
1068
  my $object = $entry->{object};
1069

  
1070
  # Check whether or not bin ID is valid.
1071
  if ($object->bin_id && !$self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }) {
1072
    push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
1073
    return 0;
1074
  }
1075

  
1076
  # Map description to ID if given.
1077
  if (!$object->bin_id && $entry->{raw_data}->{bin}) {
1078
    my $bin = $self->bins_by->{_wh_id_and_description_ident()}->{ _wh_id_and_description_maker($object->warehouse_id, $entry->{raw_data}->{bin}) };
1079
    if (!$bin) {
1080
      push @{ $entry->{errors} }, $::locale->text('Error: Invalid bin');
1081
      return 0;
1082
    }
1083

  
1084
    $object->bin_id($bin->id);
1085
  }
1086

  
1087
  if ($object->bin_id) {
1088
    $entry->{info_data}->{bin} = $self->bins_by->{_wh_id_and_id_ident()}->{ _wh_id_and_id_maker($object->warehouse_id, $object->bin_id) }->description;
1089
  } else {
1090
    push @{ $entry->{errors} }, $::locale->text('Error: Bin not found');
1091
    return 0;
1092
  }
1093

  
1094
  return 1;
1095
}
1096

  
1097
sub save_additions {
1098
  my ($self, $object) = @_;
1099

  
1100
  # record links
1101
  my $orders = delete $object->{source_orders};
1102

  
1103
  if (scalar(@$orders)) {
1104

  
1105
    $_->link_to_record($object) for @$orders;
1106

  
1107
    foreach my $item (@{ $object->items }) {
1108
      my $orderitem = delete $item->{source_item};
1109
      $orderitem->link_to_record($item) if $orderitem;
1110
    }
1111
  }
1112

  
1113
  # delivery order for all positions created?
1114
  if (scalar(@$orders)) {
1115
    foreach my $order (@{ $orders }) {
1116
      my $all_deliverd;
1117
      foreach my $orderitem (@{ $order->items }) {
1118
        my $delivered_qty = 0;
1119
        foreach my $do_item (@{$orderitem->linked_records(to => 'DeliveryOrderItem')}) {
1120
          $delivered_qty += $do_item->unit_obj->convert_to($do_item->qty, $orderitem->unit_obj);
1121
        }
1122
        $all_deliverd = $orderitem->qty <= $delivered_qty;
1123
        last if !$all_deliverd;
1124
      }
1125
      $order->update_attributes(delivered => !!$all_deliverd);
1126
    }
1127
  }
1128

  
1129
  # inventory (or use WH->transfer?)
1130
  foreach my $item (@{ $object->items }) {
1131
    foreach my $stock_info (@{ $item->delivery_order_stock_entries }) {
1132
      my $inventory  = delete $stock_info->{inventory_obj};
1133
      next if !$inventory;
1134
      my ($trans_id) = selectrow_query($::form, $object->db->dbh, qq|SELECT nextval('id')|);
1135
      $inventory->trans_id($trans_id);
1136
      $inventory->oe_id($object->id);
1137
      $inventory->delivery_order_items_stock_id($stock_info->id);
1138
      $inventory->save;
1139
    }
1140
  }
1141
}
1142

  
1143
sub save_objects {
1144
  my ($self, %params) = @_;
1145

  
1146
  # Collect orders without errors to save.
1147
  my $entries_to_save = [];
1148
  foreach my $entry (@{ $self->controller->data }) {
1149
    next if $entry->{raw_data}->{datatype} ne $self->_order_column;
1150
    next if @{ $entry->{errors} };
1151

  
1152
    push @{ $entries_to_save }, $entry;
1153
  }
1154

  
1155
  $self->SUPER::save_objects(data => $entries_to_save);
1156
}
1157

  
1158
sub get_stocked_qty {
1159
  my ($self, $parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore) = @_;
1160

  
1161
  my $key = join '+', $parts_id, $wh_id, $bin_id, $chargenumber, $bestbefore;
1162
  return $self->{stocked_qty}->{$key} if exists $self->{stocked_qty}->{$key};
1163

  
1164
  my $bestbefore_filter  = '';
1165
  my $bestbefore_val_cnt = 0;
1166
  if ($::instance_conf->get_show_bestbefore) {
1167
    $bestbefore_filter  = ($bestbefore) ? 'AND bestbefore = ?' : 'AND bestbefore IS NULL';
1168
    $bestbefore_val_cnt = ($bestbefore) ? 1                    : 0;
1169
  }
1170

  
1171
  my $query = <<SQL;
1172
    SELECT sum(qty) FROM inventory
1173
      WHERE parts_id = ? AND warehouse_id = ? AND bin_id = ? AND chargenumber = ? $bestbefore_filter
1174
      GROUP BY warehouse_id, bin_id, chargenumber
1175
SQL
1176

  
1177
  my @values = ($parts_id,
1178
                $wh_id,
1179
                $bin_id,
1180
                $chargenumber);
1181
  push @values, $bestbefore if $bestbefore_val_cnt;
1182

  
1183
  my $dbh = $self->controller->data->[0]{object}->db->dbh;
1184
  my ($stocked_qty) = selectrow_query($::form, $dbh, $query, @values);
1185

  
1186
  $self->{stocked_qty}->{$key} = $stocked_qty;
1187
  return $stocked_qty;
1188
}
1189

  
1190
sub _wh_id_and_description_ident {
1191
  return 'wh_id+description';
1192
}
1193

  
1194
sub _wh_id_and_description_maker {
1195
  return join '+', $_[0], $_[1]
1196
}
1197

  
1198
sub _wh_id_and_id_ident {
1199
  return 'wh_id+id';
1200
}
1201

  
1202
sub _wh_id_and_id_maker {
1203
  return join '+', $_[0], $_[1]
1204
}
1205

  
1206
sub _transfer_type_dir_and_description_ident {
1207
  return 'dir+description';
1208
}
1209

  
1210
sub _transfer_type_dir_and_description_maker {
1211
  return join '+', $_[0], $_[1]
1212
}
1213

  
1214
sub _order_column {
1215
  $_[0]->settings->{'order_column'}
1216
}
1217

  
1218
sub _item_column {
1219
  $_[0]->settings->{'item_column'}
1220
}
1221

  
1222
sub _stock_column {
1223
  $_[0]->settings->{'stock_column'}
1224
}
1225

  
1226
1;
doc/changelog
14 14
 - Verarbeitung von ZUGFeRD 2.0 kompatiblen Eingangsrechnungen über
15 15
   Kreditorenbuchungsvorlagen
16 16

  
17
 - CSV-Import für Lieferscheine
18

  
17 19
Kleinere neue Features und Detailverbesserungen:
18 20

  
19 21
 - Suche nach Erzeugnissen über die dort verbauten Artikel
locale/de/all
514 514
  'CSV import: bank transactions' => 'CSV Import: Bankbewegungen',
515 515
  'CSV import: contacts'        => 'CSV-Import: Ansprechpersonen',
516 516
  'CSV import: customers and vendors' => 'CSV-Import: Kunden und Lieferanten',
517
  'CSV import: delivery orders' => 'CSV-Import: Lieferscheine',
517 518
  'CSV import: inventories'     => 'CSV-Import: Lagerbewegungen/-bestände',
518 519
  'CSV import: orders'          => 'CSV-Import: Aufträge',
519 520
  'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen',
......
1001 1002
  'Delivery terms'              => 'Lieferbedingungen',
1002 1003
  'Delivery terms (database ID)' => 'Lieferbedingungen (Datenbank-ID)',
1003 1004
  'Delivery terms (name)'       => 'Lieferbedingungen (Name)',
1005
  'DeliveryOrder'               => 'Lieferschein',
1004 1006
  'Denmark'                     => 'Dänemark',
1005 1007
  'Department'                  => 'Abteilung',
1006 1008
  'Department (database ID)'    => 'Abeilung (Datenbank-ID)',
......
1275 1277
  'Equity'                      => 'Passiva',
1276 1278
  'Erfolgsrechnung'             => 'Erfolgsrechnung',
1277 1279
  'Error'                       => 'Fehler',
1280
  'Error handling'              => 'Fehlerbehandlung',
1278 1281
  'Error in database control file \'%s\': %s' => 'Fehler in Datenbankupgradekontrolldatei \'%s\': %s',
1279 1282
  'Error in position #1: You must either assign no stock at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie müssen einer Position entweder gar keinen Lagereingang oder die vollständige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
1280 1283
  'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => 'Fehler in Position #1: Sie müssen einer Position entweder gar keinen Lagerausgang oder die vollständige im Lieferschein vermerkte Menge von #2 #3 zuweisen.',
......
1296 1299
  'Error: Customer/vendor is ambiguous' => 'Kunde/Lieferant ist mehrdeutig',
1297 1300
  'Error: Customer/vendor missing' => 'Fehler: Kunde/Lieferant fehlt',
1298 1301
  'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
1302
  'Error: Faulty position in this delivery order' => 'Fehler: Fehlerhafte Artikel-Position in diesem Lieferschein',
1299 1303
  'Error: Found local bank account number but local bank code doesn\'t match' => 'Fehler: Kontonummer wurde gefunden aber gespeicherte Bankleitzahl stimmt nicht überein',
1300 1304
  'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
1301 1305
  'Error: Invalid bin'          => 'Fehler: Ungültiger Lagerplatz',
......
1322 1326
  'Error: Invalid warehouse'    => 'Fehler: Ungültiges Lager',
1323 1327
  'Error: Invalid warehouse id' => 'Ungültige Lager-ID',
1324 1328
  'Error: Invalid warehouse name #1' => 'Ungültiger Lagername \'#1\'',
1329
  'Error: More than one source order found' => 'Fehler: mehr als ein Quell-Auftrag gefunden',
1325 1330
  'Error: Name missing'         => 'Fehler: Name fehlt',
1331
  'Error: Not enough parts in stock' => 'Fehler: Nicht genügend Artikel eingelagert',
1326 1332
  'Error: Part is ambiguous'    => 'Artikel ist mehrdeutig',
1327 1333
  'Error: Part is obsolete'     => 'Fehler: Artikel ist ungültig',
1328 1334
  'Error: Part not found'       => 'Fehler: Artikel nicht gefunden',
1329 1335
  'Error: Quantity to transfer is zero.' => 'Fehler: Zu bewegende Menge ist Null.',
1336
  'Error: Source order not found' => 'Fehler: Quell-Auftrag nicht gefunden',
1337
  'Error: Stock problem'        => 'Fehler: Problem bei der Lagerbewegung',
1338
  'Error: Stocking out would result in stock underrun' => 'Auslagern würde zu einem negativen Lagerbestand führen',
1339
  'Error: Stocking out would result in stock underrun: #1' => 'Auslagern würde zu einem negativen Lagerbestand führen: #1',
1330 1340
  'Error: Transfer would result in a negative target quantity.' => 'Fehler: Lagerbewegung würde zu einer negativen Zielmenge führen.',
1331 1341
  'Error: Unit missing or invalid' => 'Fehler: Einheit fehlt oder ungültig',
1332 1342
  'Error: Warehouse not found'  => 'Fehler: Lager nicht gefunden',
......
1627 1637
  'If you want to delete such a dataset you have to edit the client(s) that are using the dataset in question and have them use another dataset.' => 'Wenn Sie eine solche Datenbank löschen möchten, dann müssen Sie zuerst den/die Mandanten auf eine andere Datenbank umstellen, die die zu löschende Datenbank benutzen.',
1628 1638
  'If you want to set up the authentication database yourself then log in to the administration panel. kivitendo will then create the database and tables for you.' => 'Wenn Sie die Authentifizierungs-Datenbank selber einrichten wollen, so melden Sie sich im Administrationsbereich an. kivitendo wird dann die Datenbank und die erforderlichen Tabellen für Sie anlegen.',
1629 1639
  'If your old bins match exactly Bins in the Warehouse CLICK on <b>AUTOMATICALLY MATCH BINS</b>.' => 'Falls die alte Lagerplatz-Beschreibung in Stammdaten genau mit einem Lagerplatz in einem vorhandenem Lager übereinstimmt, KLICK auf <b>LAGERPLÄTZE AUTOMATISCH ZUWEISEN</b>',
1640
  'Ignore faulty positions'     => 'Fehlerhafte Artikel-Positionen ignorieren',
1630 1641
  'Illegal characters have been removed from the following fields: #1' => 'Ungültige Zeichen wurden aus den folgenden Feldern entfernt: #1',
1631 1642
  'Illegal date'                => 'Ungültiges Datum',
1632 1643
  'Image'                       => 'Grafik',
......
1740 1751
  'Invoices with payments cannot be canceled.' => 'Rechnungen mit Zahlungen können nicht storniert werden.',
1741 1752
  'Invoices, Credit Notes & AR Transactions' => 'Rechnungen, Gutschriften & Debitorenbuchungen',
1742 1753
  'Is Searchable'               => 'Durchsuchbar',
1754
  'Is sales'                    => 'Verkauf',
1743 1755
  'Is this a summary account to record' => 'Sammelkonto für',
1744 1756
  'It can be changed later but must be unique within the installation.' => 'Er ist nachträglich änderbar, muss aber im System eindeutig sein.',
1745 1757
  'It is not allowed that a summary account occurs in a drop-down menu!' => 'Ein Sammelkonto darf nicht in Aufklappmenüs aufgenommen werden!',
......
2199 2211
  'Order probability & expected billing date' => 'Auftragswahrscheinlichkeit & vorrauss. Abrechnungsdatum',
2200 2212
  'Order value periodicity'     => 'Auftragswert basiert auf Periodizität',
2201 2213
  'Order/Item row name'         => 'Name der Auftrag-/Positions-Zeilen',
2214
  'Order/Item/Stock row name'   => 'Name der Auftrag-/Positions-/Lager-Zeilen',
2202 2215
  'Order/RFQ Number'            => 'Belegnummer',
2203 2216
  'OrderItem'                   => 'Position',
2204 2217
  'Ordered'                     => 'Von Kunden bestellt',
......
3061 3074
  'Stock for part #1'           => 'Bestand für Artikel #1',
3062 3075
  'Stock levels'                => 'Lagerbestände',
3063 3076
  'Stock value'                 => 'Bestandswert',
3077
  'StockInfo'                   => 'Lagerinfo',
3064 3078
  'Stocked Qty'                 => 'Lagermenge',
3065 3079
  'Stocktaking'                 => 'Inventur',
3066 3080
  'Stocktaking History'         => 'Inventur Historie',
......
3924 3938
  'Warn before saving orders without a delivery date' => 'Warnung ausgeben, falls Aufträge kein Lieferdatum haben.',
3925 3939
  'Warning'                     => 'Warnung',
3926 3940
  'Warning! Loading a draft will discard unsaved data!' => 'Achtung! Beim Laden eines Entwurfs werden ungespeicherte Daten verworfen!',
3941
  'Warning: Faulty position ignored' => 'Warnung: Fehlerhafte Artikel-Position ignoriert',
3927 3942
  'Warning: One or more field value are not in valid DATEV format at:' => 'Warnung: Ein oder mehere Felder haben ungültige Feldwerte laut DATEV-Spezifikation bei:',
3928 3943
  'Warnings and errors'         => 'Warnungen und Fehler',
3929 3944
  'Watch status'                => 'Status',
locale/en/all
514 514
  'CSV import: bank transactions' => '',
515 515
  'CSV import: contacts'        => '',
516 516
  'CSV import: customers and vendors' => '',
517
  'CSV import: delivery orders' => '',
517 518
  'CSV import: inventories'     => '',
518 519
  'CSV import: orders'          => '',
519 520
  'CSV import: parts and services' => '',
......
1001 1002
  'Delivery terms'              => '',
1002 1003
  'Delivery terms (database ID)' => '',
1003 1004
  'Delivery terms (name)'       => '',
1005
  'DeliveryOrder'               => '',
1004 1006
  'Denmark'                     => '',
1005 1007
  'Department'                  => '',
1006 1008
  'Department (database ID)'    => '',
......
1275 1277
  'Equity'                      => '',
1276 1278
  'Erfolgsrechnung'             => '',
1277 1279
  'Error'                       => '',
1280
  'Error handling'              => '',
1278 1281
  'Error in database control file \'%s\': %s' => '',
1279 1282
  'Error in position #1: You must either assign no stock at all or the full quantity of #2 #3.' => '',
1280 1283
  'Error in position #1: You must either assign no transfer at all or the full quantity of #2 #3.' => '',
......
1296 1299
  'Error: Customer/vendor is ambiguous' => '',
1297 1300
  'Error: Customer/vendor missing' => '',
1298 1301
  'Error: Customer/vendor not found' => '',
1302
  'Error: Faulty position in this delivery order' => '',
1299 1303
  'Error: Found local bank account number but local bank code doesn\'t match' => '',
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff