Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 3f924c0f

Von Kivitendo Admin vor fast 9 Jahren hinzugefügt

  • ID 3f924c0fc876c2133e5ba22f25a45a484885ee0b
  • Vorgänger 4f43ec85
  • Nachfolger fea07b8e

Debitorenbuchungen als CSV importieren

Ähnlich wie der Auftragsimport wird hier gemultiplexed, d.h. es gibt separate
Zeilen für die Debitorenbuchung (ar) und die Buchungszeilen (acc_trans).

Es handelt sich allerdings nicht exakt um acc_trans-Zeilen, die direkt
als acc_trans Objekte importiert werden, sondern es können die gleichen
Informationen wie bei der Debitorenbuchung übergeben werden, also Konto,
Betrag, Steuerschlüssel und Projekt, und daraus werden dann die
acc_trans-Zeilen generiert, inklusive Steuerautomatik.

Das Forderungskonto muß in der Rechnungszeile übergeben werden, der Betrag wird
dann anhand der Buchungszeilen bestimmt.

Beispiel für Import-Format (siehe auch mehr Beispiele in t/controllers/csvimport/artransactions.t)

datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
datatype,accno,amount,taxkey
"Rechnung",960,4,1,"invoice",f,1400
"AccTransaction",8400,159.48,3

Unterschiede anzeigen:

SL/Controller/CsvImport.pm
17 17
use SL::Controller::CsvImport::Shipto;
18 18
use SL::Controller::CsvImport::Project;
19 19
use SL::Controller::CsvImport::Order;
20
use SL::Controller::CsvImport::ARTransaction;
20 21
use SL::JSON;
21 22
use SL::Controller::CsvImport::BankTransaction;
22 23
use SL::BackgroundJob::CsvImport;
......
224 225
sub check_type {
225 226
  my ($self) = @_;
226 227

  
227
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders bank_transactions);
228
  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders bank_transactions ar_transactions);
228 229
  $self->type($::form->{profile}->{type});
229 230
}
230 231

  
......
270 271
            : $self->type eq 'projects'          ? $::locale->text('CSV import: projects')
271 272
            : $self->type eq 'orders'            ? $::locale->text('CSV import: orders')
272 273
            : $self->type eq 'bank_transactions' ? $::locale->text('CSV import: bank transactions')
274
            : $self->type eq 'ar_transactions'   ? $::locale->text('CSV import: ar transactions')
273 275
            : die;
274 276

  
275
  if ($self->{type} eq 'customers_vendors' or $self->{type} eq 'orders'  ) {
277
  if ($self->{type} eq 'customers_vendors' or $self->{type} eq 'orders' or $self->{type} eq 'ar_transactions' ) {
276 278
    $self->all_taxzones(SL::DB::Manager::TaxZone->get_all_sorted(query => [ obsolete => 0 ]));
277 279
  };
278 280

  
......
626 628
       : $self->{type} eq 'projects'          ? SL::Controller::CsvImport::Project->new(@args)
627 629
       : $self->{type} eq 'orders'            ? SL::Controller::CsvImport::Order->new(@args)
628 630
       : $self->{type} eq 'bank_transactions' ? SL::Controller::CsvImport::BankTransaction->new(@args)
631
       : $self->{type} eq 'ar_transactions'   ? SL::Controller::CsvImport::ARTransaction->new(@args)
629 632
       :                                        die "Program logic error";
630 633
}
631 634

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

  
3
use strict;
4

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

  
7
use Data::Dumper;
8
use SL::Helper::Csv;
9
use SL::Controller::CsvImport::Helper::Consistency;
10
use SL::DB::Invoice;
11
use SL::DB::AccTransaction;
12
use SL::DB::Department;
13
use SL::DB::Project;
14
use SL::DB::TaxZone;
15
use SL::DB::Chart;
16
use SL::TransNumber;
17
use DateTime;
18

  
19
use parent qw(SL::Controller::CsvImport::BaseMulti);
20

  
21
use Rose::Object::MakeMethods::Generic
22
(
23
 'scalar --get_set_init' => [ qw(settings charts_by taxkeys_by) ],
24
);
25

  
26

  
27
sub init_class {
28
  my ($self) = @_;
29
  $self->class(['SL::DB::Invoice', 'SL::DB::AccTransaction']);
30
}
31

  
32
sub set_profile_defaults {
33
  my ($self) = @_;
34

  
35
  $self->controller->profile->_set_defaults(
36
                       ar_column          => $::locale->text('Invoice'),
37
                       transaction_column => $::locale->text('AccTransaction'),
38
                       max_amount_diff    => 0.02,
39
                      );
40
};
41

  
42

  
43
sub init_settings {
44
  my ($self) = @_;
45

  
46
  return { map { ( $_ => $self->controller->profile->get($_) ) } qw(ar_column transaction_column max_amount_diff) };
47
}
48

  
49
sub init_profile {
50
  my ($self) = @_;
51

  
52
  my $profile = $self->SUPER::init_profile;
53

  
54
  # SUPER::init_profile sets row_ident to the translated class name
55
  # overwrite it with the user specified settings
56
# TODO: remove hardcoded row_idents
57
  foreach my $p (@{ $profile }) {
58
    if ($p->{class} eq 'SL::DB::Invoice') {
59
      $p->{row_ident} = $self->_ar_column;
60
    }
61
    if ($p->{class} eq 'SL::DB::AccTransaction') {
62
      $p->{row_ident} = $self->_transaction_column;
63
    }
64
  }
65

  
66
  foreach my $p (@{ $profile }) {
67
    my $prof = $p->{profile};
68
    if ($p->{row_ident} eq $self->_ar_column) {
69
      # no need to handle
70
      delete @{$prof}{qw(delivery_customer_id delivery_vendor_id )};
71
    }
72
    if ($p->{row_ident} eq $self->_transaction_column) {
73
      # no need to handle
74
      delete @{$prof}{qw(trans_id)};
75
    }
76
  }
77

  
78
  return $profile;
79
}
80

  
81

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

  
85
  $self->SUPER::setup_displayable_columns;
86

  
87
  $self->add_displayable_columns($self->_ar_column,
88
                                 { name => 'datatype',                description => $self->_ar_column . ' [1]'                               },
89
                                 { name => 'currency',                description => $::locale->text('Currency')                              },
90
                                 { name => 'cusordnumber',            description => $::locale->text('Customer Order Number')                 },
91
                                 { name => 'direct_debit',            description => $::locale->text('direct debit')                          },
92
                                 { name => 'donumber',                description => $::locale->text('Delivery Order Number')                 },
93
                                 { name => 'duedate',                 description => $::locale->text('Due Date')                              },
94
                                 { name => 'delivery_term_id',        description => $::locale->text('Delivery terms (database ID)')          },
95
                                 { name => 'delivery_term',           description => $::locale->text('Delivery terms (name)')                 },
96
                                 { name => 'deliverydate',            description => $::locale->text('Delivery Date')                         },
97
                                 { name => 'employee_id',             description => $::locale->text('Employee (database ID)')                },
98
                                 { name => 'intnotes',                description => $::locale->text('Internal Notes')                        },
99
                                 { name => 'notes',                   description => $::locale->text('Notes')                                 },
100
                                 { name => 'invnumber',               description => $::locale->text('Invoice Number')                        },
101
                                 { name => 'quonumber',               description => $::locale->text('Quotation Number')                      },
102
                                 { name => 'reqdate',                 description => $::locale->text('Reqdate')                               },
103
                                 { name => 'salesman_id',             description => $::locale->text('Salesman (database ID)')                },
104
                                 { name => 'transaction_description', description => $::locale->text('Transaction description')               },
105
                                 { name => 'transdate',               description => $::locale->text('Invoice Date')                          },
106
                                 { name => 'verify_amount',           description => $::locale->text('Amount (for verification)') . ' [2]'    },
107
                                 { name => 'verify_netamount',        description => $::locale->text('Net amount (for verification)') . ' [2]'},
108
                                 { name => 'taxincluded',             description => $::locale->text('Tax Included')                          },
109
                                 { name => 'customer',                description => $::locale->text('Customer (name)')                       },
110
                                 { name => 'customernumber',          description => $::locale->text('Customer Number')                       },
111
                                 { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
112
                                 { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
113
                                 { name => 'language',                description => $::locale->text('Language (name)')                       },
114
                                 { name => 'payment_id',              description => $::locale->text('Payment terms (database ID)')           },
115
                                 { name => 'payment',                 description => $::locale->text('Payment terms (name)')                  },
116
                                 { name => 'taxzone_id',              description => $::locale->text('Tax zone (database ID)')                },
117
                                 { name => 'taxzone',                 description => $::locale->text('Tax zone (description)')                },
118
                                 { name => 'department_id',           description => $::locale->text('Department (database ID)')              },
119
                                 { name => 'department',              description => $::locale->text('Department (description)')              },
120
                                 { name => 'globalproject_id',        description => $::locale->text('Document Project (database ID)')        },
121
                                 { name => 'globalprojectnumber',     description => $::locale->text('Document Project (number)')             },
122
                                 { name => 'globalproject',           description => $::locale->text('Document Project (description)')        },
123
                                 { name => 'archart',                 description => $::locale->text('Receivables account (account number)')  },
124
                                 { name => 'orddate',                 description => $::locale->text('Order Date')                            },
125
                                 { name => 'ordnumber',               description => $::locale->text('Order Number')                          },
126
                                 { name => 'quonumber',               description => $::locale->text('Quotation Number')                      },
127
                                 { name => 'quodate',                 description => $::locale->text('Quotation Date')                        },
128
                                );
129

  
130
  $self->add_displayable_columns($self->_transaction_column,
131
                                 { name => 'datatype',      description => $self->_transaction_column . ' [1]'       },
132
                                 { name => 'projectnumber', description => $::locale->text('Project (number)')       },
133
                                 { name => 'project',       description => $::locale->text('Project (description)')  },
134
                                 { name => 'amount',        description => $::locale->text('Amount')                 },
135
                                 { name => 'chart',         description => $::locale->text('Account number')         },
136
                                 { name => 'taxkey',        description => $::locale->text('Taxkey')                 },
137
                                );
138
}
139

  
140
sub init_taxkeys_by {
141
  my ($self) = @_;
142

  
143
  my $all_taxes = SL::DB::Manager::Tax->get_all;
144
  return { map { $_->taxkey => $_->id } @{ $all_taxes } };
145
}
146

  
147

  
148
sub init_charts_by {
149
  my ($self) = @_;
150

  
151
  my $all_charts = SL::DB::Manager::Chart->get_all;
152
  return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_charts } } ) } qw(id accno) };
153
}
154

  
155
sub check_objects {
156
  my ($self) = @_;
157

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

  
160
  my $i = 0;
161
  my $num_data = scalar @{ $self->controller->data };
162

  
163
  foreach my $entry (@{ $self->controller->data }) {
164
    $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
165

  
166
    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
167
      $self->handle_invoice($entry);
168
    } elsif ($entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
169
      $self->handle_transaction($entry);
170
    } else {
171
      die "unknown datatype";
172
    };
173

  
174
  } continue {
175
    $i++;
176
  } # finished data parsing
177

  
178
  $self->add_transactions_to_ar(); # go through all data entries again, adding receivable entry to ar lines while calculating amount and netamount
179

  
180
  foreach my $entry (@{ $self->controller->data }) {
181
    next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
182
    $self->check_verify_amounts($entry->{object});
183
  };
184

  
185
  foreach my $entry (@{ $self->controller->data }) {
186
    next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
187
    unless ( $entry->{object}->validate_acc_trans ) {
188
      push @{ $entry->{errors} }, $::locale->text('Error: ar transaction doesn\'t validate');
189
    };
190
  };
191

  
192
  # add info columns that aren't directly part of the object to be imported
193
  # but are always determined or should always be shown because they are mandatory
194
  $self->add_info_columns($self->_ar_column,
195
                          { header => $::locale->text('Customer/Vendor'),     method => 'vc_name'   },
196
                          { header => $::locale->text('Receivables account'), method => 'archart'   },
197
                          { header => $::locale->text('Amount'),              method => 'amount'    },
198
                          { header => $::locale->text('Net amount'),          method => 'netamount' },
199
                          { header => $::locale->text('Tax zone'),            method => 'taxzone'   });
200

  
201
  # Adding info_header this way only works, if the first invoice $self->controller->data->[0]
202

  
203
  # Todo: access via ->[0] ok? Better: search first order column and use this
204
  $self->add_info_columns($self->_ar_column, { header => $::locale->text('Department'),    method => 'department' }) if $self->controller->data->[0]->{info_data}->{department} or $self->controller->data->[0]->{raw_data}->{department};
205

  
206
  $self->add_info_columns($self->_ar_column, { header => $::locale->text('Project Number'), method => 'globalprojectnumber' }) if $self->controller->data->[0]->{info_data}->{globalprojectnumber};
207

  
208
  $self->add_columns($self->_ar_column,
209
                     map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment department globalproject taxzone cp currency));
210
  $self->add_columns($self->_ar_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
211
  $self->add_columns($self->_ar_column, 'notes')            if exists $self->controller->data->[0]->{raw_data}->{notes};
212

  
213
  # Todo: access via ->[1] ok? Better: search first item column and use this
214
  $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Chart'), method => 'accno' });
215
  $self->add_columns($self->_transaction_column, 'amount');
216

  
217
  $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Project Number'), method => 'projectnumber' }) if $self->controller->data->[1]->{info_data}->{projectnumber};
218

  
219
  # $self->add_columns($self->_transaction_column,
220
  #                    map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project price_factor pricegroup));
221
  # $self->add_columns($self->_transaction_column,
222
  #                    map { "${_}_id" } grep { exists $self->controller->data->[2]->{raw_data}->{$_} } qw(project price_factor pricegroup));
223
  # $self->add_columns($self->_transaction_column, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
224
  # $self->add_columns($self->_transaction_column, 'taxkey') if exists $self->controller->data->[1]->{raw_data}->{taxkey};
225

  
226
  # If invoice has errors, add error for acc_trans items
227
  # If acc_trans item has an error, add an error to the invoice item
228
  my $ar_entry;
229
  foreach my $entry (@{ $self->controller->data }) {
230
    # Search first order
231
    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
232
      $ar_entry = $entry;
233
    } elsif ( defined $ar_entry
234
              && $entry->{raw_data}->{datatype} eq $self->_transaction_column
235
              && scalar @{ $ar_entry->{errors} } > 0 ) {
236
      push @{ $entry->{errors} }, $::locale->text('Error: invalid ar row for this transaction');
237
    } elsif ( defined $ar_entry
238
              && $entry->{raw_data}->{datatype} eq $self->_transaction_column
239
              && scalar @{ $entry->{errors} } > 0 ) {
240
      push @{ $ar_entry->{errors} }, $::locale->text('Error: invalid acc transactions for this ar row');
241
    }
242
  }
243
}
244

  
245
sub handle_invoice {
246

  
247
  my ($self, $entry) = @_;
248

  
249
  my $object = $entry->{object};
250

  
251
  $object->transactions( [] ); # initialise transactions for ar object so methods work on unsaved transactions
252

  
253
  my $vc_obj;
254
  if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_id)) {
255
    $self->check_vc($entry, 'customer_id');
256
    # check_vc only sets customer_id, but we need vc_obj later for customer defaults
257
    $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
258
  } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_id)) {
259
    $self->check_vc($entry, 'vendor_id');
260
    $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
261
  } else {
262
    push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
263
  }
264

  
265
  # check for duplicate invnumbers already in database
266
  if ( SL::DB::Manager::Invoice->get_all_count( where => [ invnumber => $object->invnumber ] ) ) {
267
    push @{ $entry->{errors} }, $::locale->text('Error: invnumber already exists');
268
  }
269

  
270
  $self->check_archart($entry); # checks for receivable account
271
  # $self->check_amounts($entry); # checks and sets amount and netamount, use verify_amount and verify_netamount instead
272
  $self->check_payment($entry); # currency default from customer used below
273
  $self->check_department($entry);
274
  $self->check_taxincluded($entry);
275
  $self->check_project($entry, global => 1);
276
  $self->check_taxzone($entry); # taxzone default from customer used below
277
  $self->check_currency($entry); # currency default from customer used below
278
  $self->handle_salesman($entry);
279
  $self->handle_employee($entry);
280

  
281
  if ($vc_obj ) {
282
    # copy defaults from customer if not specified in import file
283
    foreach (qw(payment_id language_id taxzone_id currency_id)) {
284
      $object->$_($vc_obj->$_) unless $object->$_;
285
    }
286
  }
287
}
288

  
289
sub check_taxkey {
290
  my ($self, $entry, $chart) = @_;
291

  
292
  die "check_taxkey needs chart object as an argument" unless ref($chart) eq 'SL::DB::Chart';
293
  # problem: taxkey is not unique in table tax, normally one of those entries is chosen directly from a dropdown
294
  # so we check if the chart has an active taxkey, and if it matches the taxkey from the import, use the active taxkey
295
  # if the chart doesn't have an active taxkey, use the first entry from Tax that matches the taxkey
296

  
297
  my $object = $entry->{object};
298
  unless ( defined $entry->{raw_data}->{taxkey} ) {
299
    push @{ $entry->{errors} }, $::locale->text('Error: taxkey missing'); # don't just assume 0, force taxkey in import
300
    return 0;
301
  };
302

  
303
  my $tax;
304

  
305
  if ( $entry->{raw_data}->{taxkey} == $chart->get_active_taxkey->tax->taxkey ) {
306
    $tax = $chart->get_active_taxkey->tax;
307
  } else {
308
   # assume there is only one tax entry with that taxkey, can't guess
309
    $tax = SL::DB::Manager::Tax->get_first( where => [ taxkey => $entry->{raw_data}->{taxkey} ]);
310
  };
311

  
312
  unless ( $tax ) {
313
    push @{ $entry->{errors} }, $::locale->text('Error: invalid taxkey');
314
    return 0;
315
  };
316

  
317
  $object->taxkey($tax->taxkey);
318
  $object->tax_id($tax->id);
319
  return 1;
320
};
321

  
322
sub check_amounts {
323
  my ($self, $entry) = @_;
324
  # currently not used in favour of verify_amount and verify_netamount
325

  
326
  my $object = $entry->{object};
327

  
328
  unless ($entry->{raw_data}->{amount} && $entry->{raw_data}->{netamount}) {
329
    push @{ $entry->{errors} }, $::locale->text('Error: need amount and netamount');
330
    return 0;
331
  };
332
  unless ($entry->{raw_data}->{amount} * 1 && $entry->{raw_data}->{netamount} * 1) {
333
    push @{ $entry->{errors} }, $::locale->text('Error: amount and netamount need to be numeric');
334
    return 0;
335
  };
336

  
337
  $object->amount( $entry->{raw_data}->{amount} );
338
  $object->netamount( $entry->{raw_data}->{netamount} );
339
};
340

  
341
sub handle_transaction {
342
  my ($self, $entry) = @_;
343

  
344
  # Prepare acc_trans data. amount is dealt with in add_transactions_to_ar
345

  
346
  my $object = $entry->{object};
347

  
348
  $self->check_project($entry, global => 0);
349
  if ( $self->check_chart($entry) ) {
350
    my $chart_obj = SL::DB::Manager::Chart->find_by(id => $object->chart_id);
351

  
352
    unless ( $chart_obj->link =~ /AR_amount/ ) {
353
      push @{ $entry->{errors} }, $::locale->text('Error: chart isn\'t an ar_amount chart');
354
      return 0;
355
    };
356

  
357
    if ( $self->check_taxkey($entry, $chart_obj) ) {
358
      # do nothing, taxkey was assigned, just continue
359
    } else {
360
      # missing taxkey, don't do anything
361
      return 0;
362
    };
363
  } else {
364
    return 0;
365
  };
366

  
367
  # check whether taxkey and automatic taxkey match
368
  # die sprintf("taxkeys don't match: %s not equal default taxkey for chart %s: %s", $object->taxkey, $chart_obj->accno, $active_tax_for_chart->tax->taxkey) unless $object->taxkey == $active_tax_for_chart->tax->taxkey;
369

  
370
  die "no taxkey for transaction object" unless $object->taxkey or $object->taxkey == 0;
371

  
372
}
373

  
374
sub check_chart {
375
  my ($self, $entry) = @_;
376

  
377
  my $object = $entry->{object};
378

  
379
  if (any { $entry->{raw_data}->{$_} } qw(accno chart_id)) {
380

  
381
    # Check whether or not chart ID is valid.
382
    if ($object->chart_id && !$self->charts_by->{id}->{ $object->chart_id }) {
383
      push @{ $entry->{errors} }, $::locale->text('Error: invalid chart_id');
384
      return 0;
385
    }
386

  
387
    # Map number to ID if given.
388
    if (!$object->chart_id && $entry->{raw_data}->{accno}) {
389
      my $chart = $self->charts_by->{accno}->{ $entry->{raw_data}->{accno} };
390
      if (!$chart) {
391
        push @{ $entry->{errors} }, $::locale->text('Error: invalid chart (accno)');
392
        return 0;
393
      }
394

  
395
      $object->chart_id($chart->id);
396
    }
397

  
398
    # Map description to ID if given.
399
    if (!$object->chart_id && $entry->{raw_data}->{description}) {
400
      my $chart = $self->charts_by->{description}->{ $entry->{raw_data}->{description} };
401
      if (!$chart) {
402
        push @{ $entry->{errors} }, $::locale->text('Error: invalid chart');
403
        return 0;
404
      }
405

  
406
      $object->chart_id($chart->id);
407
    }
408

  
409
    if ($object->chart_id) {
410
      # add account number to preview
411
      $entry->{info_data}->{accno} = $self->charts_by->{id}->{ $object->chart_id }->accno;
412
    } else {
413
      push @{ $entry->{errors} }, $::locale->text('Error: chart not found');
414
      return 0;
415
    }
416
  } else {
417
    push @{ $entry->{errors} }, $::locale->text('Error: chart missing');
418
    return 0;
419
  }
420

  
421
  return 1;
422
}
423

  
424
sub check_archart {
425
  my ($self, $entry) = @_;
426

  
427
  my $chart;
428

  
429
  if ( $entry->{raw_data}->{archart} ) {
430
    my $archart = $entry->{raw_data}->{archart};
431
    $chart = SL::DB::Manager::Chart->find_by(accno => $archart);
432
    unless ($chart) {
433
      push @{ $entry->{errors} }, $::locale->text("Error: can't find ar chart with accno #1", $archart);
434
      return 0;
435
    };
436
  } elsif ( $::instance_conf->get_ar_chart_id ) {
437
    $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
438
  } else {
439
    push @{ $entry->{errors} }, $::locale->text("Error: neither archart passed, no default receivables chart configured");
440
    return 0;
441
  };
442

  
443
  unless ($chart->link eq 'AR') {
444
    push @{ $entry->{errors} }, $::locale->text('Error: archart isn\'t an AR chart');
445
    return 0;
446
  };
447

  
448
  $entry->{info_data}->{archart} = $chart->accno;
449
  $entry->{object}->{archart} = $chart;
450
  return 1;
451
};
452

  
453
sub check_taxincluded {
454
  my ($self, $entry) = @_;
455

  
456
  my $object = $entry->{object};
457

  
458
  if ( $entry->{raw_data}->{taxincluded} ) {
459
    if ( $entry->{raw_data}->{taxincluded} eq 'f' or $entry->{raw_data}->{taxincluded} eq '0' ) {
460
      $object->taxincluded('0');
461
    } elsif ( $entry->{raw_data}->{taxincluded} eq 't' or $entry->{raw_data}->{taxincluded} eq '1' ) {
462
      $object->taxincluded('1');
463
    } else {
464
      push @{ $entry->{errors} }, $::locale->text('Error: taxincluded has to be t or f');
465
      return 0;
466
    };
467
  } else {
468
    push @{ $entry->{errors} }, $::locale->text('Error: taxincluded wasn\'t set');
469
    return 0;
470
  };
471
  return 1;
472
};
473

  
474
sub check_verify_amounts {
475
  my ($self) = @_;
476

  
477
  # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
478
  # And throw an error if the differences are too big.
479
  my @to_verify = ( { column      => 'amount',
480
                      raw_column  => 'verify_amount',
481
                      info_header => 'Calc. Amount',
482
                      info_method => 'calc_amount',
483
                      err_msg     => $::locale->text('Amounts differ too much'),
484
                    },
485
                    { column      => 'netamount',
486
                      raw_column  => 'verify_netamount',
487
                      info_header => 'Calc. Net amount',
488
                      info_method => 'calc_netamount',
489
                      err_msg     => $::locale->text('Net amounts differ too much'),
490
                    } );
491

  
492
  foreach my $tv (@to_verify) {
493
    if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
494
      $self->add_raw_data_columns($self->_ar_column, $tv->{raw_column});
495
      $self->add_info_columns($self->_ar_column,
496
                              { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
497
    }
498

  
499
    # check differences
500
    foreach my $entry (@{ $self->controller->data }) {
501
      if ( @{ $entry->{errors} } ) {
502
        push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
503
        return 0;
504
      };
505

  
506
      if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
507
        next if !$entry->{raw_data}->{ $tv->{raw_column} };
508
        my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
509
        # round $abs_diff, otherwise it might trigger for 0.020000000000021
510
        my $abs_diff = $::form->round_amount(abs($entry->{object}->${ \$tv->{column} } - $parsed_value),2);
511
        if ( $abs_diff > $self->settings->{'max_amount_diff'}) {
512
          push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
513
        }
514
      }
515
    }
516
  }
517
};
518

  
519
sub add_transactions_to_ar {
520
  my ($self) = @_;
521

  
522
  # go through all verified ar and acc_trans rows in import, adding acc_trans objects to ar objects
523

  
524
  my $ar_entry;  # the current ar row
525

  
526
  foreach my $entry (@{ $self->controller->data }) {
527
    # when we reach an ar_column for the first time, don't do anything, just store in $ar_entry
528
    # when we reach an ar_column for the second time, save it
529
    if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
530
      if ( $ar_entry && $ar_entry->{object} ) { # won't trigger the first time, finishes the last object
531
        if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
532
          $ar_entry->{object}->recalculate_amounts; # determine and set amount and netamount for ar
533
          $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
534
          $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
535
          $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
536
        } else {
537
          push @{ $entry->{errors} }, $::locale->text("ar_chart isn't a valid chart");
538
        };
539
      };
540
      $ar_entry = $entry; # remember as last ar_entry
541

  
542
    } elsif ( defined $ar_entry && $entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
543
      push @{ $entry->{errors} }, $::locale->text('no tax_id in acc_trans')   unless $entry->{object}->tax_id || $entry->{object}->tax_id == 0;
544
      next if @{ $entry->{errors} };
545

  
546
      my $acc_trans_objects = $ar_entry->{object}->add_ar_amount_row(
547
        amount      => $entry->{object}->amount,
548
        chart       => SL::DB::Manager::Chart->find_by(id => $entry->{object}->chart_id), # add_ar_amount takes chart obj. as argument
549
        tax_id      => $entry->{object}->tax_id,
550
        project_id  => $entry->{object}->project_id,
551
        debug       => 0,
552
      );
553

  
554
    } else {
555
      die "This should never happen\n";
556
    };
557
  }
558

  
559
  # finish the final object
560
  if ( $ar_entry->{object} ) {
561
    if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
562
      $ar_entry->{object}->recalculate_amounts;
563
      $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
564
      $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
565

  
566
      $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
567
    } else {
568
      push @{ $ar_entry->{errors} }, $::locale->text("The receivables chart isn't a valid chart.");
569
      return 0;
570
    };
571
  } else {
572
    die "There was no final ar_entry object";
573
  };
574
}
575

  
576
sub save_objects {
577
  my ($self, %params) = @_;
578

  
579
  # save all the Invoice objects
580
  my $objects_to_save;
581
  foreach my $entry (@{ $self->controller->data }) {
582
    # only push the invoice objects that don't have an error
583
    next if $entry->{raw_data}->{datatype} ne $self->_ar_column;
584
    next if @{ $entry->{errors} };
585

  
586
    die unless $entry->{object}->validate_acc_trans;
587

  
588
    push @{ $objects_to_save }, $entry;
589
  }
590

  
591
  $self->SUPER::save_objects(data => $objects_to_save);
592
}
593

  
594
sub _ar_column {
595
  $_[0]->settings->{'ar_column'}
596
}
597

  
598
sub _transaction_column {
599
  $_[0]->settings->{'transaction_column'}
600
}
601

  
602
1;
SL/Controller/CsvImport/Base.pm
522 522
sub _save_history {
523 523
  my ($self, $object) = @_;
524 524

  
525
  if (any { $_ eq $self->controller->{type} } qw(parts customers_vendors orders)) {
525
  if (any { $_ eq $self->controller->{type} } qw(parts customers_vendors orders ar_transactions)) {
526 526
    my $snumbers = $self->controller->{type} eq 'parts'             ? 'partnumber_' . $object->partnumber
527 527
                 : $self->controller->{type} eq 'customers_vendors' ?
528 528
                     ($self->table eq 'customer' ? 'customernumber_' . $object->customernumber : 'vendornumber_' . $object->vendornumber)
529 529
                 : $self->controller->{type} eq 'orders'            ? 'ordnumber_' . $object->ordnumber
530
                 : $self->controller->{type} eq 'ar_transactions'   ? 'invnumber_' . $object->invnumber
530 531
                 : '';
531 532

  
532 533
    my $what_done = $self->controller->{type} eq 'orders' ? 'sales_order'
SL/Controller/CsvImport/BaseMulti.pm
187 187
    eval "require " . $class;
188 188

  
189 189
    my %unwanted = map { ( $_ => 1 ) } (qw(itime mtime), map { $_->name } @{ $class->meta->primary_key_columns });
190

  
191
    # TODO: execeptions for AccTransaction and Invoice wh
192
    if ( $class =~ m/^SL::DB::AccTransaction/ ) {
193
      my %unwanted_acc_trans = map { ( $_ => 1 ) } (qw(acc_trans_id trans_id cleared fx_transaction ob_transaction cb_transaction itime mtime chart_link tax_id description gldate memo source transdate), map { $_->name } @{ $class->meta->primary_key_columns });
194
      @unwanted{keys %unwanted_acc_trans} = values %unwanted_acc_trans;
195
    };
196
    if ( $class =~ m/^SL::DB::Invoice/ ) {
197
      # remove fields that aren't needed / shouldn't be set for ar transaction
198
      my %unwanted_ar = map { ( $_ => 1 ) } (qw(closed currency currency_id datepaid dunning_config_id gldate invnumber_for_credit_note invoice marge_percent marge_total amount netamount paid shippingpoint shipto_id shipvia storno storno_id type cp_id), map { $_->name } @{ $class->meta->primary_key_columns });
199
      @unwanted{keys %unwanted_ar} = values %unwanted_ar;
200
    };
201

  
190 202
    my %prof;
191 203
    $prof{datatype} = '';
192 204
    for my $col ($class->meta->columns) {
......
268 280

  
269 281
  my %field_lengths_by_ri = $self->field_lengths;
270 282
  foreach my $entry (@{ $self->controller->data }) {
271
    next unless @{ $entry->{errors} };
283
    next unless defined $entry->{errors} && @{ $entry->{errors} };
272 284
    my %field_lengths = %{ $field_lengths_by_ri{ $entry->{raw_data}->{datatype} } };
273 285
    map { $entry->{object}->$_(substr($entry->{object}->$_, 0, $field_lengths{$_})) if $entry->{object}->$_ } keys %field_lengths;
274 286
  }
doc/changelog
40 40
  zu Lieferscheinposition mitverfolgt. Ferner wird der Nettowarenwert für den Fall
41 41
  Hauptwährung und Netto-Auftrag berechnet.
42 42

  
43
Debitorenbuchungsimport
44

  
45
  Neuer Menüpunkt im CSV Importer. Anwendungsbeispiele:
46
  * bei einer Migration zu kivitendo die offenen Posten übernehmen
47
  * wenn kivitendo für die Buchhaltung benutzt wird, die Rechnungen aber mit
48
    einem externen Programm erstellt werden
49

  
43 50
Kleinere neue Features und Detailverbesserungen:
44 51

  
45 52
  - Neues Feld GLN bei Kunden/Lieferanten und Lieferadressen.
locale/de/all
74 74
  'AR Aging'                    => 'Offene Forderungen',
75 75
  'AR Transaction'              => 'Debitorenbuchung',
76 76
  'AR Transaction (abbreviation)' => 'D',
77
  'AR Transaction/AccTrans Item row names' => 'Namen der Rechnungs/Buchungszeilen',
77 78
  'AR Transactions'             => 'Debitorenbuchungen',
78 79
  'AR transactions changeable'  => 'Änderbarkeit von Debitorenbuchungen',
79 80
  'ASSETS'                      => 'AKTIVA',
......
245 246
  'Amount payable'              => 'Noch zu bezahlender Betrag',
246 247
  'Amount payable less discount' => 'Noch zu bezahlender Betrag abzüglich Skonto',
247 248
  'An error occured. Letter could not be deleted.' => 'Es ist ein Fehler aufgetreten. Der Brief konnte nicht gelöscht werden.',
249
  'Amounts differ too much'     => 'Beträge weichen zu sehr voneinander ab.',
248 250
  'An exception occurred during execution.' => 'Während der Ausführung trat eine Ausnahme auf.',
249 251
  'An invalid character was used (invalid characters: #1).' => 'Ein ungültiges Zeichen wurde benutzt (ungültige Zeichen: #1).',
250 252
  'An invalid character was used (valid characters: #1).' => 'Ein ungültiges Zeichen wurde benutzt (gültige Zeichen: #1).',
......
444 446
  'CSS style for pictures'      => 'CSS Style für Bilder',
445 447
  'CSV'                         => 'CSV',
446 448
  'CSV export -- options'       => 'CSV-Export -- Optionen',
449
  'CSV import: ar transactions' => 'CSV Import: Debitorenbuchungen',
447 450
  'CSV import: bank transactions' => 'CSV Import: Bankbewegungen',
448 451
  'CSV import: contacts'        => 'CSV-Import: Ansprechpersonen',
449 452
  'CSV import: customers and vendors' => 'CSV-Import: Kunden und Lieferanten',
......
516 519
  'Charge number'               => 'Chargennummer',
517 520
  'Charset'                     => 'Zeichensatz',
518 521
  'Chart'                       => 'Buchungskonto',
522
  'Chart (database ID)'         => 'Konto (Datenbank ID)',
519 523
  'Chart Type'                  => 'Kontentyp',
520 524
  'Chart balance'               => 'Kontensaldo',
521 525
  'Chart of Accounts'           => 'Kontenübersicht',
......
1130 1134
  'Error: Customer/vendor not found' => 'Fehler: Kunde/Lieferant nicht gefunden',
1131 1135
  'Error: Found local bank account number but local bank code doesn\'t match' => 'Fehler: Kontonummer wurde gefunden aber gespeicherte Bankleitzahl stimmt nicht überein',
1132 1136
  'Error: Gender (cp_gender) missing or invalid' => 'Fehler: Geschlecht (cp_gender) fehlt oder ungültig',
1137
  'Error: Invalid ar transaction for this order item' => 'Fehler: ungültige Debitorenrechnung für diese Buchungszeile',
1133 1138
  'Error: Invalid bin'          => 'Fehler: Ungültiger Lagerplatz',
1134 1139
  'Error: Invalid business'     => 'Fehler: Kunden-/Lieferantentyp ungültig',
1140
  'Error: Invalid chart'        => 'Fehler: ungültiges Konto',
1135 1141
  'Error: Invalid contact'      => 'Fehler: Ansprechperson ungültig',
1136 1142
  'Error: Invalid currency'     => 'Fehler: ungültige Währung',
1137 1143
  'Error: Invalid delivery terms' => 'Fehler: Lieferbedingungen ungültig',
......
1156 1162
  'Error: Transfer would result in a negative target quantity.' => 'Fehler: Lagerbewegung würde zu einer negativen Zielmenge führen.',
1157 1163
  'Error: Unit missing or invalid' => 'Fehler: Einheit fehlt oder ungültig',
1158 1164
  'Error: Warehouse not found'  => 'Fehler: Lager nicht gefunden',
1165
  'Error: amount and netamount need to be numeric' => 'Fehler: amount und netamount müssen numerisch sein',
1166
  'Error: ar transaction doesn\'t validate' => 'Fehler: die Debitorenbuchung ist nicht korrekt',
1167
  'Error: archart isn\'t an AR chart' => 'Fehler: das Forderungskonto ist nicht als Forderungskonto definiert (link = AR)',
1168
  'Error: can\'t find ar chart with accno #1' => 'Fehler: kein Forderungskonto mit Kontonummer #1',
1169
  'Error: chart isn\'t an ar_amount chart' => 'Fehler: Konto ist kein Erlöskonto',
1170
  'Error: chart missing'        => 'Fehler: Konto fehlt',
1171
  'Error: chart not found'      => 'Fehler: Konto nicht gefunden',
1172
  'Error: invalid acc transactions for this ar row' => 'Fehler: ungültige Buchungszeilen für diese Rechnungzeile',
1173
  'Error: invalid ar row for this transaction' => 'Ungültige Rechnungszeile für diese Buchungszeile',
1174
  'Error: invalid chart'        => 'Fehler: ungültiges Konto',
1175
  'Error: invalid chart (accno)' => 'Fehler: ungültiges Konto (accno)',
1176
  'Error: invalid chart_id'     => 'Fehler: ungültige Konto ID (chart_id)',
1177
  'Error: invalid taxkey'       => 'Fehler: ungültiger Steuerschlüssel',
1178
  'Error: invnumber already exists' => 'Fehler: Rechnungsnummer existiert schon',
1159 1179
  'Error: local bank account id doesn\'t match local bank account number' => 'Fehler: Bankkonto-ID stimmt nicht mit Kontonummer überein',
1160 1180
  'Error: local bank account id doesn\'t match local bank code' => 'Fehler: Bankkonto-ID stimmt nicht mit BLZ überein',
1181
  'Error: need amount and netamount' => 'Fehler: amount und netamount werden benötigt',
1182
  'Error: neither archart passed, no default receivables chart configured' => 'Fehler: Forderungskonto (archart) fehlt, kein Standardforderungskonto definiert',
1183
  'Error: taxincluded has to be t or f' => 'Fehler: Steuer im Preis inbegriffen muß t oder f sein',
1184
  'Error: taxincluded wasn\'t set' => 'Fehler: Steuer im Preis inbegriffen nicht gesetzt (taxincluded)',
1185
  'Error: taxkey missing'       => 'Fehler: Steuerschlüssel fehlt',
1161 1186
  'Error: this feature requires that articles with a time-based unit (e.g. \'h\' or \'min\') exist.' => 'Fehler: dieses Feature setzt voraus, dass Artikel mit einer Zeit-basierenden Einheit (z.B. "Std") existieren.',
1162 1187
  'Error: unknown local bank account' => 'Fehler: unbekannte Kontnummer',
1163 1188
  'Error: unknown local bank account id' => 'Fehler: unbekannte Bankkonto-ID',
......
1680 1705
  'Net Value in delivery orders' => 'Netto mit Lieferschein',
1681 1706
  'Net amount'                  => 'Nettobetrag',
1682 1707
  'Net amount (for verification)' => 'Nettobetrag (zur Überprüfung)',
1708
  'Net amounts differ too much' => 'Nettobeträge weichen zu sehr ab.',
1683 1709
  'Net value in Order'          => 'Netto Auftrag',
1684 1710
  'Net value transferred in / out' => 'Netto ein- /ausgelagert',
1685 1711
  'Net value without delivery orders' => 'Netto ohne Lieferschein',
......
2173 2199
  'Receipts'                    => 'Zahlungseingänge',
2174 2200
  'Receivable account'          => 'Forderungskonto',
2175 2201
  'Receivables'                 => 'Forderungen',
2176
  'Recipients'                  => 'EmpfängerInnen',
2202
  'Receivables account'         => 'Forderungskonto',
2203
  'Receivables account (account number)' => 'Forderungskonto (Kontonummer)',
2177 2204
  'Reconcile'                   => 'Abgleichen',
2178 2205
  'Reconciliation'              => 'Kontenabgleich',
2179 2206
  'Reconciliation with bank'    => 'Kontenabgleich mit Bank',
......
2708 2735
  'The client has been deleted.' => 'Der Mandant wurde gelöscht.',
2709 2736
  'The client has been saved.'  => 'Der Mandant wurde gespeichert.',
2710 2737
  'The clipboard does not contain anything that can be pasted here.' => 'Die Zwischenablage enthält momentan keine Objekte, die hier eingefügt werden können.',
2738
  'The column "datatype" must be present and must be at the same position / column in each data set. The values must be the row names (see settings) for invoice and transaction data respectively.' => 'Die Spalte "datatype" muss vorhanden sein und sie muss in jedem Datensatz an der gleichen Stelle / Spalte sein. Die Werte in dieser Spalte müssen die Namen der Rechnungs- und Buchungszeilen (siehe Einstellungen) sein.',
2711 2739
  'The column "datatype" must be present and must be at the same position / column in each data set. The values must be the row names (see settings) for order and item data respectively.' => 'Die Spalte "datatype" muss vorhanden sein und sie muss in jedem Datensatz an der gleichen Stelle / Spalte sein. Die Werte in dieser Spalte müssen die Namen der Auftrag-/Positions-Zeilen (siehe Einstellungen) sein.',
2712 2740
  'The column "make_X" can contain either a vendor\'s database ID, a vendor number or a vendor\'s name.' => 'Die Spalte "make_X" can entweder die Datenbank-ID des Lieferanten, eine Lieferantennummer oder einen Lieferantennamen enthalten.',
2713 2741
  'The column triplets can occur multiple times with different numbers "X" each time (e.g. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).' => 'Die Spalten-Dreiergruppen können mehrfach auftreten, sofern sie unterschiedliche Nummern "X" verwenden (z.B. "make_1", "model_1", "lastcost_1", "make_2", "model_2", "lastcost_2", "make_3", "model_3", "lastcost_3" etc).',
......
2858 2886
  'The project type has been deleted.' => 'Der Projekttyp wurde gelöscht.',
2859 2887
  'The project type has been saved.' => 'Der Projekttyp wurde gespeichert.',
2860 2888
  'The project type is in use and cannot be deleted.' => 'Der Projekttyp wird verwendet und kann nicht gelöscht werden.',
2889
  'The receivables chart isn\'t a valid chart.' => 'Das Forderungskonto ist kein gültiges Konto',
2861 2890
  'The recipient, subject or body is missing.' => 'Der Empfäger, der Betreff oder der Text ist leer.',
2862 2891
  'The required information consists of the IBAN and the BIC.' => 'Die benötigten Informationen bestehen aus der IBAN und der BIC. Zusätzlich wird die SEPA-Kreditoren-Identifikation aus der Mandantenkonfiguration benötigt.',
2863 2892
  'The required information consists of the IBAN, the BIC, the mandator ID and the mandate\'s date of signature.' => 'Die benötigten Informationen bestehen aus IBAN, BIC, Mandanten-ID und dem Unterschriftsdatum des Mandates. Zusätzlich wird die SEPA-Kreditoren-Identifikation aus der Mandantenkonfiguration benötigt.',
......
3323 3352
  'and'                         => 'und',
3324 3353
  'ap_aging_list'               => 'liste_offene_verbindlichkeiten',
3325 3354
  'ar_aging_list'               => 'liste_offene_forderungen',
3355
  'ar_chart isn\'t a valid chart' => 'Das Forderungskonto ist kein gültiges Konto.',
3326 3356
  'as at'                       => 'zum Stand',
3327 3357
  'assembly'                    => 'Erzeugnis',
3328 3358
  'assembly_list'               => 'erzeugnisliste',
menus/user/00-erp.yaml
1257 1257
  params:
1258 1258
    action: CsvImport/new
1259 1259
    profile.type: orders
1260
- parent: system_import_csv
1261
  id: system_import_csv_ar_transactions
1262
  name: AR Transactions
1263
  order: 800
1264
  params:
1265
    action: CsvImport/new
1266
    profile.type: ar_transactions
1260 1267
- parent: system
1261 1268
  id: system_templates
1262 1269
  name: Templates
t/controllers/csvimport/artransactions.t
1
use Test::More tests => 70;
2

  
3
use strict;
4

  
5
use lib 't';
6

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

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

  
15
my $DEBUG = 0;
16

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

  
19
use SL::DB::Buchungsgruppe;
20
use SL::DB::Currency;
21
use SL::DB::Customer;
22
use SL::DB::Employee;
23
use SL::DB::Invoice;
24
use SL::DB::TaxZone;
25
use SL::DB::Chart;
26
use SL::DB::AccTransaction;
27

  
28
my ($customer, $currency_id, $employee, $taxzone, $project, $department);
29

  
30
sub reset_state {
31
  # Create test data
32
  my %params = @_;
33

  
34
  $params{$_} ||= {} for qw(buchungsgruppe customer tax);
35

  
36
  clear_up();
37
  $employee        = SL::DB::Manager::Employee->current                          || croak "No employee";
38
  $taxzone         = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || croak "No taxzone";
39
  $currency_id     = $::instance_conf->get_currency_id;
40

  
41
  $customer     = SL::DB::Customer->new(
42
    name        => 'Test Customer',
43
    currency_id => $currency_id,
44
    taxzone_id  => $taxzone->id,
45
    %{ $params{customer} }
46
  )->save;
47

  
48
  $project     = SL::DB::Project->new(
49
    projectnumber  => 'P1',
50
    description    => 'Project X',
51
    project_type   => SL::DB::Manager::ProjectType->find_by(description => 'Standard'),
52
    project_status => SL::DB::Manager::ProjectStatus->find_by(name => 'running'),
53
  )->save;
54

  
55
  $department     = SL::DB::Department->new(
56
    description    => 'Department 1',
57
  )->save;
58
}
59

  
60
Support::TestSetup::login();
61

  
62
reset_state(customer => {id => 960, customernumber => 2});
63

  
64
#####
65
sub test_import {
66
  my $file = shift;
67

  
68
  my $controller = SL::Controller::CsvImport->new();
69

  
70
  my $csv_artransactions_import = SL::Controller::CsvImport::ARTransaction->new(
71
    settings    => {'ar_column'          => 'Rechnung',
72
                    'transaction_column' => 'AccTransaction',
73
                    'max_amount_diff'    => 0.02
74
                  },
75
    controller => $controller,
76
    file       => $file,
77
  );
78

  
79
  # $csv_artransactions_import->init_vc_by;
80
  $csv_artransactions_import->test_run(0);
81
  $csv_artransactions_import->csv(SL::Helper::Csv->new(file                    => $csv_artransactions_import->file,
82
                                                       profile                 => $csv_artransactions_import->profile,
83
                                                       encoding                => 'utf-8',
84
                                                       ignore_unknown_columns  => 1,
85
                                                       strict_profile          => 1,
86
                                                       case_insensitive_header => 1,
87
                                                       sep_char                => ',',
88
                                                       quote_char              => '"',
89
                                                       ignore_unknown_columns  => 1,
90
                                                     ));
91

  
92
  $csv_artransactions_import->csv->parse;
93

  
94
  $csv_artransactions_import->controller->errors([ $csv_artransactions_import->csv->errors ]) if $csv_artransactions_import->csv->errors;
95

  
96
  return if ( !$csv_artransactions_import->csv->header || $csv_artransactions_import->csv->errors );
97

  
98
  my $headers;
99
  my $i = 0;
100
  foreach my $header (@{ $csv_artransactions_import->csv->header }) {
101

  
102
    my $profile   = $csv_artransactions_import->csv->profile->[$i]->{profile};
103
    my $row_ident = $csv_artransactions_import->csv->profile->[$i]->{row_ident};
104

  
105
    my $h = { headers => [ grep { $profile->{$_} } @{ $header } ] };
106
    $h->{methods} = [ map { $profile->{$_} } @{ $h->{headers} } ];
107
    $h->{used}    = { map { ($_ => 1) }      @{ $h->{headers} } };
108

  
109
    $headers->{$row_ident} = $h;
110
    $i++;
111
  }
112

  
113
  $csv_artransactions_import->controller->headers($headers);
114

  
115
  my $raw_data_headers;
116
  my $info_headers;
117
  foreach my $p (@{ $csv_artransactions_import->csv->profile }) {
118
    my $ident = $p->{row_ident};
119
    $raw_data_headers->{$ident} = { used => { }, headers => [ ] };
120
    $info_headers->{$ident}     = { used => { }, headers => [ ] };
121
  }
122
  $csv_artransactions_import->controller->raw_data_headers($raw_data_headers);
123
  $csv_artransactions_import->controller->info_headers($info_headers);
124

  
125
  my $objects  = $csv_artransactions_import->csv->get_objects;
126
  my @raw_data = @{ $csv_artransactions_import->csv->get_data };
127

  
128
  $csv_artransactions_import->controller->data([ pairwise { no warnings 'once'; { object => $a, raw_data => $b, errors => [], information => [], info_data => {} } } @$objects, @raw_data ]);
129
  $csv_artransactions_import->check_objects;
130

  
131
  # don't try and save objects that have errors
132
  $csv_artransactions_import->save_objects unless scalar @{$csv_artransactions_import->controller->data->[0]->{errors}};
133

  
134
 return $csv_artransactions_import->controller->data;
135
}
136

  
137
##### manually create an ar transaction from scratch, testing the methods
138
$::myconfig{numberformat} = '1000.00';
139
my $old_locale = $::locale;
140
# set locale to en so we can match errors
141
$::locale = Locale->new('en');
142

  
143
my $amount = 10;
144

  
145
my $ar = SL::DB::Invoice->new(
146
  invoice      => 0,
147
  invnumber    => 'manual invoice',
148
  taxzone_id   => $taxzone->id,
149
  currency_id  => $currency_id,
150
  taxincluded  => 'f',
151
  customer_id  => $customer->id,
152
  transdate    => DateTime->today,
153
  employee_id  => SL::DB::Manager::Employee->current->id,
154
  transactions => [],
155
);
156

  
157
my $tax3 = SL::DB::Manager::Tax->find_by(rate => 0.19, taxkey => 3) || die "can't find tax with taxkey 3";
158
my $income_chart = SL::DB::Manager::Chart->find_by(accno => '8400') || die "can't find income chart";
159

  
160
$ar->add_ar_amount_row(
161
  amount => $amount,
162
  chart  => $income_chart,
163
  tax_id => $tax3->id,
164
);
165

  
166
$ar->recalculate_amounts; # set amount and netamount from transactions
167
is $ar->amount, '10', 'amount of manual invoice is 10';
168
is $ar->netamount, '8.4', 'netamount of manual invoice is 10';
169

  
170
$ar->create_ar_row( chart => SL::DB::Manager::Chart->find_by(accno => '1400', link => 'AR') );
171
my $result = $ar->validate_acc_trans(debug => 0);
172
is $result, 1, 'manual $ar validates';
173

  
174
$ar->save;
175
is ${ $ar->transactions }[0]->chart->accno, '8400', 'assigned income chart after save ok';
176
is ${ $ar->transactions }[2]->chart->accno, '1400', 'assigned receivable chart after save ok';
177
is scalar @{$ar->transactions}, 3, 'manual invoice has 3 acc_trans entries';
178

  
179
$ar->pay_invoice(  chart_id      => SL::DB::Manager::Chart->find_by(accno => '1200')->id, # bank
180
                   amount        => $ar->open_amount,
181
                   transdate     => DateTime->now->to_kivitendo,
182
                   payment_type  => 'without_skonto',  # default if not specified
183
                  );
184
$result = $ar->validate_acc_trans(debug => 0);
185
is $result, 1, 'manual invoice validates after payment';
186

  
187
reset_state(customer => {id => 960, customernumber => 2});
188

  
189
my ($entries, $entry, $file);
190

  
191
# starting test of csv imports
192
# to debug errors in certain tests, run after test_import:
193
#   die Dumper($entry->{errors});
194
##### basic test
195
$file = \<<EOL;
196
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
197
datatype,accno,amount,taxkey
198
"Rechnung",960,4,1,"invoice 1",f,1400
199
"AccTransaction",8400,159.48,3
200
EOL
201
$entries = test_import($file);
202
$entry = $entries->[0];
203
$entry->{object}->validate_acc_trans;
204

  
205
is $entry->{object}->invnumber, 'invoice 1', 'simple invnumber ok (customer)';
206
is $entry->{object}->customer_id, '960', 'simple customer_id ok (customer)';
207
is scalar @{$entry->{object}->transactions}, 3, 'invoice 1 has 3 acc_trans entries';
208
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), 159.48, 'invoice 1 ar amount is 159.48';
209
is $entry->{object}->direct_debit, '0', 'no direct debit';
210
is $entry->{object}->taxincluded, '0', 'taxincluded is false';
211
is $entry->{object}->amount, '189.78', 'ar amount tax not included is 189.78';
212
is $entry->{object}->netamount, '159.48', 'ar netamount tax not included is 159.48';
213

  
214
##### test for duplicate invnumber
215
$file = \<<EOL;
216
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
217
datatype,accno,amount,taxkey
218
"Rechnung",960,4,1,"invoice 1",f,1400
219
"AccTransaction",8400,159.48,3
220
EOL
221
$entries = test_import($file);
222
$entry = $entries->[0];
223
$entry->{object}->validate_acc_trans;
224
is $entry->{errors}->[0], 'Error: invnumber already exists', 'detects verify_amount differences';
225

  
226
##### test for no invnumber given
227
$file = \<<EOL;
228
datatype,customer_id,taxzone_id,currency_id,taxincluded,archart
229
datatype,accno,amount,taxkey
230
"Rechnung",960,4,1,f,1400
231
"AccTransaction",8400,159.48,3
232
EOL
233
$entries = test_import($file);
234
$entry = $entries->[0];
235
$entry->{object}->validate_acc_trans;
236
is $entry->{object}->invnumber =~ /^\d+$/, 1, 'invnumber assigned automatically';
237

  
238
##### basic test without amounts in Rechnung, only specified in AccTransaction
239
$file = \<<EOL;
240
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
241
datatype,accno,amount,taxkey
242
"Rechnung",960,4,1,"invoice 1 no amounts",f,1400
243
"AccTransaction",8400,159.48,3
244
EOL
245
$entries = test_import($file);
246
$entry = $entries->[0];
247
$entry->{object}->validate_acc_trans;
248

  
249
is $entry->{object}->invnumber, 'invoice 1 no amounts', 'simple invnumber ok (customer)';
250
is $entry->{object}->customer_id, '960', 'simple customer_id ok (customer)';
251
is scalar @{$entry->{object}->transactions}, 3, 'invoice 1 has 3 acc_trans entries';
252
is $::form->round_amount($entry->{object}->amount, 2), '189.78', 'not taxincluded ar amount';
253
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '159.48', 'not taxincluded acc_trans netamount';
254
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), 159.48, 'invoice 1 ar amount is 159.48';
255

  
256
##### basic test: credit_note
257
$file = \<<EOL;
258
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
259
datatype,accno,amount,taxkey
260
"Rechnung",960,4,1,"credit note",f,1400
261
"AccTransaction",8400,-159.48,3
262
EOL
263
$entries = test_import($file);
264
$entry = $entries->[0];
265
$entry->{object}->validate_acc_trans;
266

  
267
is $entry->{object}->invnumber, 'credit note', 'simple credit note ok';
268
is scalar @{$entry->{object}->transactions}, 3, 'credit note has 3 acc_trans entries';
269
is $::form->round_amount($entry->{object}->amount, 2), '-189.78', 'taxincluded ar amount';
270
is $::form->round_amount($entry->{object}->netamount, 2), '-159.48', 'taxincluded ar net amount';
271
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), -159.48, 'credit note ar amount is -159.48';
272
is $entry->{object}->amount, '-189.78', 'credit note amount tax not included is 189.78';
273
is $entry->{object}->netamount, '-159.48', 'credit note netamount tax not included is 159.48';
274

  
275
#### verify_amount differs: max_amount_diff = 0.02, 189.80 is ok, 189.81 is not
276
$file = \<<EOL;
277
datatype,customer_id,verify_amount,verify_netamount,taxzone_id,currency_id,invnumber,taxincluded,archart
278
datatype,accno,amount,taxkey
279
"Rechnung",960,189.81,159.48,4,1,"invoice amounts differing",f,1400
280
"AccTransaction",8400,159.48,3
281
EOL
282
$entries = test_import($file);
283
$entry = $entries->[0];
284
is $entry->{errors}->[0], 'Amounts differ too much', 'detects verify_amount differences';
285

  
286
#####  direct debit
287
$file = \<<EOL;
288
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,direct_debit,archart
289
datatype,accno,amount,taxkey
290
"Rechnung",960,4,1,"invoice with direct debit",f,t,1400
291
"AccTransaction",8400,159.48,3
292
EOL
293

  
294
$entries = test_import($file);
295
$entry = $entries->[0];
296
$entry->{object}->validate_acc_trans;
297
is $entry->{object}->direct_debit, '1', 'direct debit';
298

  
299
#### tax included
300
$file = \<<EOL;
301
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
302
datatype,accno,amount,taxkey
303
"Rechnung",960,4,1,"invoice 1 tax included no amounts",t,1400
304
"AccTransaction",8400,189.78,3
305
EOL
306

  
307
$entries = test_import($file);
308
$entry = $entries->[0];
309
$entry->{object}->validate_acc_trans(debug => 0);
310
is $entry->{object}->taxincluded, '1', 'taxincluded is true';
311
is $::form->round_amount($entry->{object}->amount, 2), '189.78', 'taxincluded ar amount';
312
is $::form->round_amount($entry->{object}->netamount, 2), '159.48', 'taxincluded ar net amount';
313
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '159.48', 'taxincluded acc_trans netamount';
314

  
315
#### multiple tax included
316
$file = \<<EOL;
317
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
318
datatype,accno,amount,taxkey
319
"Rechnung",960,4,1,"invoice multiple tax included",t,1400
320
"AccTransaction",8400,94.89,3
321
"AccTransaction",8400,94.89,3
322
EOL
323

  
324
$entries = test_import($file);
325
$entry = $entries->[0];
326
$entry->{object}->validate_acc_trans;
327
is $::form->round_amount($entry->{object}->amount, 2),    '189.78', 'taxincluded ar amount';
328
is $::form->round_amount($entry->{object}->netamount, 2), '159.48', 'taxincluded ar netamount';
329
is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '79.74', 'taxincluded amount';
330
is $::form->round_amount($entry->{object}->transactions->[1]->amount, 2), '15.15', 'taxincluded tax';
331

  
332
# different receivables chart
333
$file = \<<EOL;
334
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
335
datatype,accno,amount,taxkey
336
"Rechnung",960,4,1,"invoice mit archart 1448",f,1448
337
"AccTransaction",8400,159.48,3
338
EOL
339
$entries = test_import($file);
340
$entry = $entries->[0];
341
$entry->{object}->validate_acc_trans;
342
is $entry->{object}->transactions->[2]->chart->accno, '1448', 'archart set to 1448';
343

  
344
# missing customer
345
$file = \<<EOL;
346
datatype,taxzone_id,currency_id,invnumber,taxincluded,archart
347
datatype,accno,amount,taxkey
348
"Rechnung",4,1,"invoice missing customer",f,1400
349
"AccTransaction",8400,159.48,3
350
EOL
351
$entries = test_import($file);
352
$entry = $entries->[0];
353
is $entry->{errors}->[0], 'Error: Customer/vendor missing', 'detects missing customer or vendor';
354

  
355

  
356
##### customer by name
357
$file = \<<EOL;
358
datatype,customer,taxzone_id,currency_id,invnumber,taxincluded,archart
359
datatype,accno,amount,taxkey
360
"Rechnung","Test Customer",4,1,"invoice customer name",f,1400
361
"AccTransaction",8400,159.48,3
362
EOL
363
$entries = test_import($file);
364
$entry = $entries->[0];
365
$entry->{object}->validate_acc_trans;
366
is $entry->{object}->customer->name, "Test Customer", 'detects customer by name';
367

  
368
##### detect missing chart
369
$file = \<<EOL;
370
datatype,taxzone_id,currency_id,invnumber,customer,archart
371
datatype,amount,taxkey
372
"Rechnung",4,1,"invoice missing chart","Test Customer",1400
373
"AccTransaction",4,3
374
EOL
375
$entries = test_import($file);
376
$entry = $entries->[1];
377
is $entry->{errors}->[0], 'Error: chart missing', 'detects missing chart (chart_id or accno)';
378

  
379
##### detect illegal chart by accno
380
$file = \<<EOL;
381
datatype,taxzone_id,currency_id,invnumber,customer,archart
382
datatype,accno,amount,taxkey
383
"Rechnung",4,1,"invoice illegal chart accno","Test Customer",1400
384
"AccTransaction",9999,4,3
385
EOL
386
$entries = test_import($file);
387
$entry = $entries->[1];
388
is $entry->{errors}->[0], 'Error: invalid chart (accno)', 'detects invalid chart (chart_id or accno)';
389

  
390
# ##### detect illegal archart
391
$file = \<<EOL;
392
datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
393
datatype,accno,amount,taxkey
394
"Rechnung",4,1,"invoice illegal archart","Test Customer",f,11400
395
"AccTransaction",8400,159.48,3
396
EOL
397
$entries = test_import($file);
398
$entry = $entries->[0];
399
is $entry->{errors}->[0], "Error: can't find ar chart with accno 11400", 'detects illegal receivables chart (archart)';
400

  
401
##### detect chart by id
402
$file = \<<EOL;
403
datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
404
datatype,amount,chart_id,taxkey
405
"Rechnung",4,1,"invoice chart_id","Test Customer",f,1400
406
"AccTransaction",159.48,184,3
407
EOL
408
$entries = test_import($file);
409
$entry = $entries->[1]; # acc_trans entry is at entry array pos 1
410
$entries->[0]->{object}->validate_acc_trans;
411
is $entry->{object}->chart->id, "184", 'detects chart by id';
412

  
413
##### detect chart by accno
414
$file = \<<EOL;
415
datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
416
datatype,amount,accno,taxkey
417
"Rechnung",4,1,"invoice by chart accno","Test Customer",f,1400
418
"AccTransaction",159.48,8400,3
419
EOL
420
$entries = test_import($file);
421
$entry = $entries->[1];
422
$entries->[0]->{object}->validate_acc_trans;
423
is $entry->{object}->chart->accno, "8400", 'detects chart by accno';
424

  
425
##### detect chart isn't an ar_chart
426
$file = \<<EOL;
427
datatype,taxzone_id,currency_id,invnumber,customer,taxincluded,archart
428
datatype,amount,accno,taxkey
429
"Rechnung",4,1,"invoice by chart accno","Test Customer",f,1400
430
"AccTransaction",159.48,1400,3
431
EOL
432
$entries = test_import($file);
433
$entry = $entries->[1];
434
$entries->[0]->{object}->validate_acc_trans;
435
is $entry->{errors}->[0], 'Error: chart isn\'t an ar_amount chart', 'detects valid chart that is not an ar_amount chart';
436

  
437
# missing taxkey
438
$file = \<<EOL;
439
datatype,taxzone_id,currency_id,invnumber,customer,archart
440
datatype,amount,accno
441
"Rechnung",4,1,"invoice missing taxkey chart accno","Test Customer",1400
442
"AccTransaction",159.48,8400
443
EOL
444
$entries = test_import($file);
445
$entry = $entries->[1];
446
is $entry->{errors}->[0], 'Error: taxkey missing', 'detects missing taxkey (DATEV Steuerschlüssel)';
447

  
448
# illegal taxkey
449
$file = \<<EOL;
450
datatype,taxzone_id,currency_id,invnumber,customer,archart
451
datatype,amount,accno,taxkey
452
"Rechnung",4,1,"invoice illegal taxkey","Test Customer",1400
453
"AccTransaction",4,8400,123
454
EOL
455
$entries = test_import($file);
456
$entry = $entries->[1];
457
is $entry->{errors}->[0], 'Error: invalid taxkey', 'detects invalid taxkey (DATEV Steuerschlüssel)';
458

  
459
# taxkey
460
$file = \<<EOL;
461
datatype,customer_id,taxzone_id,currency_id,invnumber,archart
462
datatype,accno,amount,taxkey
463
"Rechnung",960,4,1,"invoice by taxkey",1400
464
"AccTransaction",8400,4,3
465
EOL
466

  
467
$entries = test_import($file);
468
$entry = $entries->[1];
469
is $entry->{object}->taxkey, 3, 'detects taxkey';
470

  
471
# acc_trans project
472
$file = \<<EOL;
473
datatype,customer_id,taxzone_id,currency_id,invnumber,archart,taxincluded
474
datatype,accno,amount,taxkey,projectnumber
475
"Rechnung",960,4,1,"invoice with acc_trans project",1400,f
476
"AccTransaction",8400,159.48,3,P1
477
EOL
478

  
479
$entries = test_import($file);
480
$entry = $entries->[1];
481
# die Dumper($entries->[0]->{errors}) if scalar @{$entries->[0]->{errors}};
482
is $entry->{object}->project->projectnumber, 'P1', 'detects acc_trans project';
483

  
484
#####  various tests
485
$file = \<<EOL;
486
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
487
datatype,accno,amount,taxkey,projectnumber
488
"Rechnung",960,4,1,"invoice various",t,1400,21.04.2016,30.04.2016,P1,Department 1
489
"AccTransaction",8400,119,3,P1
490
"AccTransaction",8300,107,2,P1
491
"AccTransaction",8200,100,0,P1
492
EOL
493

  
494
$entries = test_import($file);
495
$entry = $entries->[0];
496
$entry->{object}->validate_acc_trans;
497
is $entry->{object}->duedate->to_kivitendo,      '30.04.2016',    'duedate';
498
is $entry->{object}->transdate->to_kivitendo,    '21.04.2016',    'transdate';
499
is $entry->{object}->globalproject->description, 'Project X',     'project';
500
is $entry->{object}->department->description,    'Department 1',  'department';
501
# 8300 is third entry after 8400 and tax for 8400
502
is $::form->round_amount($entry->{object}->transactions->[2]->amount),     '100',        '8300 net amount: 100';
503
is $::form->round_amount($entry->{object}->transactions->[2]->taxkey),     '2',          '8300 has taxkey 2';
504
is $::form->round_amount($entry->{object}->transactions->[2]->project_id), $project->id, 'AccTrans project';
505

  
506
#####  ar amount test
507
$file = \<<EOL;
508
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
509
datatype,accno,amount,taxkey,projectnumber
510
"Rechnung",960,4,1,"invoice various 1",t,1400,21.04.2016,30.04.2016,P1,Department 1
511
"AccTransaction",8400,119,3,P1
512
"AccTransaction",8300,107,2,P1
513
"AccTransaction",8200,100,0,P1
514
"Rechnung",960,4,1,"invoice various 2",t,1400,21.04.2016,30.04.2016,P1,Department 1
515
"AccTransaction",8400,119,3,P1
516
"AccTransaction",8300,107,2,P1
517
"AccTransaction",8200,100,0,P1
518
EOL
519

  
520
$entries = test_import($file);
521
$entry = $entries->[0];
522
$entry->{object}->validate_acc_trans;
523
is $entry->{object}->duedate->to_kivitendo,      '30.04.2016',    'duedate';
524
is $entry->{info_data}->{amount}, '326', "First invoice amount displayed in info data";
525
is $entries->[4]->{info_data}->{amount}, '326', "Second invoice amount displayed in info data";
526

  
527
# multiple entries, taxincluded = f
528
$file = \<<EOL;
529
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
530
datatype,accno,amount,taxkey
531
"Rechnung",960,4,1,"invoice 4 acc_trans",f,1400
532
"AccTransaction",8400,39.87,3
533
"AccTransaction",8400,39.87,3
534
"AccTransaction",8400,39.87,3
535
"AccTransaction",8400,39.87,3
536
"Rechnung",960,4,1,"invoice 4 acc_trans 2",f,1400
537
"AccTransaction",8400,39.87,3
538
"AccTransaction",8400,39.87,3
539
"AccTransaction",8400,39.87,3
540
"AccTransaction",8400,39.87,3
541
"Rechnung",960,4,1,"invoice 4 acc_trans 3",f,1400
542
"AccTransaction",8400,39.87,3
543
"AccTransaction",8400,39.87,3
544
"AccTransaction",8400,39.87,3
545
"AccTransaction",8400,39.87,3
546
"Rechnung",960,4,1,"invoice 4 acc_trans 4",f,1448
547
"AccTransaction",8400,39.87,3
548
"AccTransaction",8400,39.87,3
549
"AccTransaction",8400,39.87,3
550
"AccTransaction",8400,39.87,3
551
EOL
552
$entries = test_import($file);
553

  
554
my $i = 0;
555
foreach my $entry ( @$entries ) {
556
  next unless $entry->{object}->isa('SL::DB::Invoice');
557
  $i++;
558
  is scalar @{$entry->{object}->transactions}, 9, "invoice $i: 'invoice 4 acc_trans' has 9 acc_trans entries";
559
  $entry->{object}->validate_acc_trans;
560
};
561

  
562
##### missing acc_trans
563
$file = \<<EOL;
564
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart,transdate,duedate,globalprojectnumber,department
565
datatype,accno,amount,taxkey,projectnumber
566
"Rechnung",960,4,1,"invoice acc_trans missing",t,1400,21.04.2016,30.04.2016,P1,Department 1
567
"Rechnung",960,4,1,"invoice various a",t,1400,21.04.2016,30.04.2016,P1,Department 1
568
"AccTransaction",8400,119,3,P1
569
"AccTransaction",8300,107,2,P1
570
EOL
571

  
572
$entries = test_import($file);
573
$entry = $entries->[0];
574
is $entry->{errors}->[0], "Error: ar transaction doesn't validate", 'detects invalid ar, maybe acc_trans entry missing';
575

  
576
my $number_of_imported_invoices = SL::DB::Manager::Invoice->get_all_count;
577
is $number_of_imported_invoices, 19, 'All invoices saved';
578

  
579
#### taxkey differs from active_taxkey
580
$file = \<<EOL;
581
datatype,customer_id,taxzone_id,currency_id,invnumber,taxincluded,archart
582
datatype,accno,amount,taxkey
583
"Rechnung",960,4,1,"invoice 1 tax included no amounts",t,1400
584
"AccTransaction",8400,189.78,2
585
EOL
586

  
587
$entries = test_import($file);
588
$entry = $entries->[0];
589
$entry->{object}->validate_acc_trans(debug => 0);
590

  
591
clear_up(); # remove all data at end of tests
592
# end of tests
593

  
594

  
595
sub clear_up {
596
  SL::DB::Manager::AccTransaction->delete_all(all => 1);
597
  SL::DB::Manager::Invoice->delete_all       (all => 1);
598
  SL::DB::Manager::Customer->delete_all      (all => 1);
599
  SL::DB::Manager::Project->delete_all       (all => 1);
600
  SL::DB::Manager::Department->delete_all    (all => 1);
601
};
602

  
603

  
604
1;
605

  
606
#####
607
# vim: ft=perl
608
# set emacs to perl mode
609
# Local Variables:
610
# mode: perl
611
# End:
templates/webpages/csv_import/_form_artransactions.html
1
[% USE LxERP %]
... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.

Auch abrufbar als: Unified diff