Revision 3f924c0f
Von Kivitendo Admin vor fast 9 Jahren hinzugefügt
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 %] |
Auch abrufbar als: Unified diff
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