1 package SL::Controller::CsvImport::APTransaction;
5 use List::MoreUtils qw(any);
8 use SL::Controller::CsvImport::Helper::Consistency;
9 use SL::DB::PurchaseInvoice;
10 use SL::DB::AccTransaction;
11 use SL::DB::Department;
18 use parent qw(SL::Controller::CsvImport::BaseMulti);
20 use Rose::Object::MakeMethods::Generic
22 'scalar --get_set_init' => [ qw(settings charts_by taxkeys_by) ],
28 $self->class(['SL::DB::PurchaseInvoice', 'SL::DB::AccTransaction']);
31 sub set_profile_defaults {
34 $self->controller->profile->_set_defaults(
35 ap_column => $::locale->text('Invoice'),
36 transaction_column => $::locale->text('AccTransaction'),
37 max_amount_diff => 0.02,
38 dont_save_anything_on_errors => 0,
46 return { map { ( $_ => $self->controller->profile->get($_) ) } qw(ap_column transaction_column max_amount_diff dont_save_anything_on_errors) };
52 my $profile = $self->SUPER::init_profile;
54 # SUPER::init_profile sets row_ident to the translated class name
55 # overwrite it with the user specified settings
56 foreach my $p (@{ $profile }) {
57 if ($p->{class} eq 'SL::DB::PurchaseInvoice') {
58 $p->{row_ident} = $self->_ap_column;
60 if ($p->{class} eq 'SL::DB::AccTransaction') {
61 $p->{row_ident} = $self->_transaction_column;
65 foreach my $p (@{ $profile }) {
66 my $prof = $p->{profile};
67 if ($p->{row_ident} eq $self->_ap_column) {
69 delete @{$prof}{qw(amount cp_id datepaid delivery_term_id gldate invoice language_id netamount paid shipvia storno storno_id taxzone taxzone_id type)};
71 if ($p->{row_ident} eq $self->_transaction_column) {
73 delete @{$prof}{qw(acc_trans_id cb_transaction chart_link cleared fx_transaction gldate memo ob_transaction source tax_id description trans_id transdate)};
80 sub init_existing_objects {
83 # only use objects of main class (the first one)
84 #eval "require " . $self->class->[0];
85 $self->existing_objects($self->manager_class->[0]->get_all);
88 sub get_duplicate_check_fields {
90 vendor_and_invnumber => {
91 label => $::locale->text('Vendor and Invoice Number'),
95 my ($object, $worker) = @_;
96 return if ref $object ne $worker->class->[0];
97 return '__' . $object->vendor_id . '__' . $object->invnumber . '__';
103 sub check_std_duplicates {
108 my $all_fields = $self->get_duplicate_check_fields();
110 foreach my $key (keys(%{ $all_fields })) {
111 if ( $self->controller->profile->get('duplicates_'. $key) && (!exists($all_fields->{$key}->{std_check}) || $all_fields->{$key}->{std_check} ) ) {
112 $duplicates->{$key} = {};
116 my @duplicates_keys = keys(%{ $duplicates });
118 if ( !scalar(@duplicates_keys) ) {
122 if ( $self->controller->profile->get('duplicates') eq 'check_db' ) {
123 foreach my $object (@{ $self->existing_objects }) {
124 foreach my $key (@duplicates_keys) {
125 my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
126 $duplicates->{$key}->{$value} = 'db';
131 # only check main class (the first one)
132 foreach my $entry (@{ $self->controller->data }) {
133 my $object = $entry->{object};
135 next if ref $object ne $self->class->[0];
136 next if scalar @{ $entry->{errors} };
138 foreach my $key (@duplicates_keys) {
139 my $value = exists($all_fields->{$key}->{maker}) ? $all_fields->{$key}->{maker}->($object, $self) : $object->$key;
141 if ( exists($duplicates->{$key}->{$value}) ) {
142 push(@{ $entry->{errors} }, $duplicates->{$key}->{$value} eq 'db' ? $::locale->text('Duplicate in database') : $::locale->text('Duplicate in CSV file'));
145 $duplicates->{$key}->{$value} = 'csv';
151 sub setup_displayable_columns {
154 $self->SUPER::setup_displayable_columns;
156 $self->add_displayable_columns($self->_ap_column,
157 { name => 'datatype', description => $self->_ap_column . ' [1]' },
158 { name => 'currency_id', description => $::locale->text('Currency (database ID)') },
159 { name => 'currency', description => $::locale->text('Currency') },
160 { name => 'deliverydate', description => $::locale->text('Delivery Date') },
161 { name => 'department_id', description => $::locale->text('Department (database ID)') },
162 { name => 'department', description => $::locale->text('Department (description)') },
163 { name => 'direct_debit', description => $::locale->text('direct debit') },
164 { name => 'duedate', description => $::locale->text('Due Date') },
165 { name => 'employee_id', description => $::locale->text('Employee (database ID)') },
166 { name => 'exchangerate', description => $::locale->text('Exchangerate') },
167 { name => 'globalproject_id', description => $::locale->text('Document Project (database ID)') },
168 { name => 'globalprojectnumber', description => $::locale->text('Document Project (number)') },
169 { name => 'globalproject', description => $::locale->text('Document Project (description)') },
170 { name => 'intnotes', description => $::locale->text('Internal Notes') },
171 { name => 'invnumber', description => $::locale->text('Invoice Number') },
172 { name => 'is_sepa_blocked', description => $::locale->text('Bank transfer via SEPA is blocked') },
173 { name => 'notes', description => $::locale->text('Notes') },
174 { name => 'orddate', description => $::locale->text('Order Date') },
175 { name => 'ordnumber', description => $::locale->text('Order Number') },
176 { name => 'payment_id', description => $::locale->text('Payment terms (database ID)') },
177 { name => 'payment', description => $::locale->text('Payment terms (name)') },
178 { name => 'quonumber', description => $::locale->text('Quotation Number') },
179 { name => 'quodate', description => $::locale->text('Quotation Date') },
180 { name => 'tax_point', description => $::locale->text('Tax point') },
181 { name => 'taxincluded', description => $::locale->text('Tax Included') },
182 { name => 'transaction_description', description => $::locale->text('Transaction description') },
183 { name => 'transdate', description => $::locale->text('Invoice Date') },
184 { name => 'vendor', description => $::locale->text('Vendor (name)') },
185 { name => 'vendornumber', description => $::locale->text('Vendor Number') },
186 { name => 'vendor_gln', description => $::locale->text('Vendor GLN') },
187 { name => 'vendor_id', description => $::locale->text('Vendor (database ID)') },
188 { name => 'verify_amount', description => $::locale->text('Amount (for verification)') . ' [2]' },
189 { name => 'verify_netamount', description => $::locale->text('Net amount (for verification)') . ' [2]' },
190 { name => 'apchart', description => $::locale->text('Payable account (account number)') },
193 $self->add_displayable_columns($self->_transaction_column,
194 { name => 'datatype', description => $self->_transaction_column . ' [1]' },
195 { name => 'amount', description => $::locale->text('Amount') },
196 { name => 'chart_id', description => $::locale->text('Account number (database ID)') },
197 { name => 'project_id', description => $::locale->text('Project (database ID)') },
198 { name => 'project', description => $::locale->text('Project (description)') },
199 { name => 'projectnumber', description => $::locale->text('Project (number)') },
200 { name => 'taxkey', description => $::locale->text('Taxkey') },
201 { name => 'accno', description => $::locale->text('Account number') },
205 sub init_taxkeys_by {
208 my $all_taxes = SL::DB::Manager::Tax->get_all;
209 return { map { $_->taxkey => $_->id } @{ $all_taxes } };
216 my $all_charts = SL::DB::Manager::Chart->get_all;
217 return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_charts } } ) } qw(id accno) };
223 $self->controller->track_progress(phase => 'building data', progress => 0);
226 my $num_data = scalar @{ $self->controller->data };
229 foreach my $entry (@{ $self->controller->data }) {
230 $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
232 if ($entry->{raw_data}->{datatype} eq $self->_ap_column) {
233 $entry->{info_data}->{datatype} = $::locale->text($self->_ap_column);
234 $self->handle_invoice($entry);
235 $invoice_entry = $entry;
236 } elsif ($entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
237 die "Cannot process transaction row without an invoice row" if !$invoice_entry;
238 $entry->{info_data}->{datatype} = $::locale->text($self->_transaction_column);
239 $self->handle_transaction($entry, $invoice_entry);
241 die "unknown datatype";
246 } # finished data parsing
248 $self->add_info_columns($self->_ap_column, { header => 'datatype', method => 'datatype' });
249 $self->add_info_columns($self->_transaction_column, { header => 'datatype', method => 'datatype' });
251 $self->add_transactions_to_ap(); # go through all data entries again, adding payable entry to ap lines while calculating amount and netamount
253 $self->check_verify_amounts();
255 foreach my $entry (@{ $self->controller->data }) {
256 next unless ($entry->{raw_data}->{datatype} eq $self->_ap_column);
257 unless ( $entry->{object}->validate_acc_trans ) {
258 push @{ $entry->{errors} }, $::locale->text('Error: ap transaction doesn\'t validate');
262 # add info columns that aren't directly part of the object to be imported
263 # but are always determined or should always be shown because they are mandatory
264 $self->add_info_columns($self->_ap_column,
265 { header => $::locale->text('Vendor'), method => 'vc_name' },
266 { header => $::locale->text('Payable account'), method => 'apchart' },
267 { header => $::locale->text('Amount'), method => 'amount' },
268 { header => $::locale->text('Net amount'), method => 'netamount' },
269 { header => $::locale->text('Tax zone'), method => 'taxzone' });
271 # Adding info_header this way only works, if the first invoice $self->controller->data->[0]
273 # Todo: access via ->[0] ok? Better: search first order column and use this
274 $self->add_info_columns($self->_ap_column, { header => $::locale->text('Department'), method => 'department' }) if $self->controller->data->[0]->{info_data}->{department} or $self->controller->data->[0]->{raw_data}->{department};
276 $self->add_info_columns($self->_ap_column, { header => $::locale->text('Project Number'), method => 'globalprojectnumber' }) if $self->controller->data->[0]->{info_data}->{globalprojectnumber};
278 $self->add_columns($self->_ap_column,
279 map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment department globalproject taxzone currency));
280 $self->add_columns($self->_ap_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
281 $self->add_columns($self->_ap_column, 'notes') if exists $self->controller->data->[0]->{raw_data}->{notes};
283 # Todo: access via ->[1] ok? Better: search first item column and use this
284 $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Chart'), method => 'accno' });
285 $self->add_columns($self->_transaction_column, 'amount');
287 $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Project Number'), method => 'projectnumber' }) if $self->controller->data->[1]->{info_data}->{projectnumber};
289 # If requested to not save anything on errors, set all ap rows without error to an error
290 if ($self->controller->profile->get('dont_save_anything_on_errors')) {
291 my $any_errors = any { scalar @{ $_->{errors} } } @{ $self->controller->data };
293 foreach my $entry (grep { ($_->{raw_data}->{datatype} eq $self->_ap_column) && !scalar @{ $_->{errors} } } @{ $self->controller->data }) {
294 push @{ $entry->{errors} }, $::locale->text('There are some errors in the file and it was requested to not save any datasets on errors.');
299 # If invoice has errors, add error for acc_trans items
300 # If acc_trans item has an error, add an error to the invoice item
302 foreach my $entry (@{ $self->controller->data }) {
304 if ($entry->{raw_data}->{datatype} eq $self->_ap_column) {
306 } elsif ( defined $ap_entry
307 && $entry->{raw_data}->{datatype} eq $self->_transaction_column
308 && scalar @{ $ap_entry->{errors} } > 0 ) {
309 push @{ $entry->{errors} }, $::locale->text('Error: invalid ap row for this transaction');
310 } elsif ( defined $ap_entry
311 && $entry->{raw_data}->{datatype} eq $self->_transaction_column
312 && scalar @{ $entry->{errors} } > 0 ) {
313 push @{ $ap_entry->{errors} }, $::locale->text('Error: invalid acc transactions for this ap row');
320 my ($self, $entry) = @_;
322 my $object = $entry->{object};
324 $object->transactions( [] ); # initialise transactions for ap object so methods work on unsaved transactions
327 if (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_gln vendor_id)) {
328 $self->check_vc($entry, 'vendor_id');
329 $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
331 push @{ $entry->{errors} }, $::locale->text('Error: Vendor missing');
334 if (!$entry->{raw_data}->{invnumber}) {
335 push @{ $entry->{errors} }, $::locale->text('Error: Invoice Number missing');
338 $self->check_apchart($entry); # checks for payable account
339 $self->check_payment($entry); # currency default from vendor used below
340 $self->check_department($entry);
341 $self->check_taxincluded($entry);
342 $self->check_project($entry, global => 1);
343 $self->check_taxzone($entry); # taxzone default from customer used below
344 $self->check_currency($entry); # currency default from customer used below
345 $self->handle_employee($entry);
348 # copy defaults from customer if not specified in import file
349 foreach (qw(payment_id language_id taxzone_id currency_id)) {
350 $object->$_($vc_obj->$_) unless $object->$_;
356 my ($self, $entry, $invoice_entry, $chart) = @_;
358 die "check_taxkey needs chart object as an argument" unless ref($chart) eq 'SL::DB::Chart';
359 # problem: taxkey is not unique in table tax, normally one of those entries is chosen directly from a dropdown
360 # so we check if the chart has an active taxkey, and if it matches the taxkey from the import, use the active taxkey
361 # if the chart doesn't have an active taxkey, use the first entry from Tax that matches the taxkey
363 my $object = $entry->{object};
364 my $invoice_object = $invoice_entry->{object};
366 unless ( defined $entry->{raw_data}->{taxkey} ) {
367 push @{ $entry->{errors} }, $::locale->text('Error: taxkey missing'); # don't just assume 0, force taxkey in import
371 my $tax = $chart->get_active_taxkey($invoice_object->deliverydate // $invoice_object->transdate // DateTime->today_local)->tax;
372 if ( $entry->{raw_data}->{taxkey} != $tax->taxkey ) {
373 # assume there is only one tax entry with that taxkey, can't guess
374 $tax = SL::DB::Manager::Tax->get_first( where => [ taxkey => $entry->{raw_data}->{taxkey} ]);
378 push @{ $entry->{errors} }, $::locale->text('Error: invalid taxkey');
382 $object->taxkey($tax->taxkey);
383 $object->tax_id($tax->id);
387 sub handle_transaction {
388 my ($self, $entry, $invoice_entry) = @_;
390 # Prepare acc_trans data. amount is dealt with in add_transactions_to_ap
392 my $object = $entry->{object};
394 $self->check_project($entry, global => 0);
395 if ( $self->check_chart($entry) ) {
396 my $chart_obj = SL::DB::Manager::Chart->find_by(id => $object->chart_id);
398 unless ( $chart_obj->link =~ /AP_amount/ ) {
399 push @{ $entry->{errors} }, $::locale->text('Error: chart isn\'t an ap_amount chart');
403 if ( $self->check_taxkey($entry, $invoice_entry, $chart_obj) ) {
404 # do nothing, taxkey was assigned, just continue
406 # missing taxkey, don't do anything
413 # check whether taxkey and automatic taxkey match
414 # 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;
416 die "no taxkey for transaction object" unless $object->taxkey or $object->taxkey == 0;
421 my ($self, $entry) = @_;
423 my $object = $entry->{object};
425 if (any { $entry->{raw_data}->{$_} } qw(accno chart_id)) {
427 # Check whether or not chart ID is valid.
428 if ($object->chart_id && !$self->charts_by->{id}->{ $object->chart_id }) {
429 push @{ $entry->{errors} }, $::locale->text('Error: invalid chart_id');
433 # Map number to ID if given.
434 if (!$object->chart_id && $entry->{raw_data}->{accno}) {
435 my $chart = $self->charts_by->{accno}->{ $entry->{raw_data}->{accno} };
437 push @{ $entry->{errors} }, $::locale->text('Error: invalid chart (accno)');
441 $object->chart_id($chart->id);
444 # Map description to ID if given.
445 if (!$object->chart_id && $entry->{raw_data}->{description}) {
446 my $chart = $self->charts_by->{description}->{ $entry->{raw_data}->{description} };
448 push @{ $entry->{errors} }, $::locale->text('Error: invalid chart');
452 $object->chart_id($chart->id);
455 if ($object->chart_id) {
456 # add account number to preview
457 $entry->{info_data}->{accno} = $self->charts_by->{id}->{ $object->chart_id }->accno;
459 push @{ $entry->{errors} }, $::locale->text('Error: chart not found');
463 push @{ $entry->{errors} }, $::locale->text('Error: chart missing');
471 my ($self, $entry) = @_;
475 if ( $entry->{raw_data}->{apchart} ) {
476 my $apchart = $entry->{raw_data}->{apchart};
477 $chart = SL::DB::Manager::Chart->find_by(accno => $apchart);
479 push @{ $entry->{errors} }, $::locale->text("Error: can't find ap chart with accno #1", $apchart);
482 } elsif ( $::instance_conf->get_ap_chart_id ) {
483 $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ap_chart_id);
485 push @{ $entry->{errors} }, $::locale->text("Error: neither apchart passed, no default payable chart configured");
489 unless ($chart->link eq 'AP') {
490 push @{ $entry->{errors} }, $::locale->text('Error: apchart isn\'t an AP chart');
494 $entry->{info_data}->{apchart} = $chart->accno;
495 $entry->{object}->{apchart} = $chart;
499 sub check_taxincluded {
500 my ($self, $entry) = @_;
502 my $object = $entry->{object};
504 if ( $entry->{raw_data}->{taxincluded} ) {
505 if ( $entry->{raw_data}->{taxincluded} eq 'f' or $entry->{raw_data}->{taxincluded} eq '0' ) {
506 $object->taxincluded('0');
507 } elsif ( $entry->{raw_data}->{taxincluded} eq 't' or $entry->{raw_data}->{taxincluded} eq '1' ) {
508 $object->taxincluded('1');
510 push @{ $entry->{errors} }, $::locale->text('Error: taxincluded has to be t or f');
514 push @{ $entry->{errors} }, $::locale->text('Error: taxincluded wasn\'t set');
520 sub check_verify_amounts {
523 # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
524 # And throw an error if the differences are too big.
525 my @to_verify = ( { column => 'amount',
526 raw_column => 'verify_amount',
527 info_header => 'Calc. Amount',
528 info_method => 'calc_amount',
529 err_msg => $::locale->text('Amounts differ too much'),
531 { column => 'netamount',
532 raw_column => 'verify_netamount',
533 info_header => 'Calc. Net amount',
534 info_method => 'calc_netamount',
535 err_msg => $::locale->text('Net amounts differ too much'),
538 foreach my $tv (@to_verify) {
539 if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
540 $self->add_raw_data_columns($self->_ap_column, $tv->{raw_column});
541 $self->add_info_columns($self->_ap_column,
542 { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
546 foreach my $entry (@{ $self->controller->data }) {
547 if ($entry->{raw_data}->{datatype} eq $self->_ap_column) {
548 next if !$entry->{raw_data}->{ $tv->{raw_column} };
549 my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
550 # round $abs_diff, otherwise it might trigger for 0.020000000000021
551 my $abs_diff = $::form->round_amount(abs($entry->{object}->${ \$tv->{column} } - $parsed_value),2);
552 if ( $abs_diff > $self->settings->{'max_amount_diff'}) {
553 push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
560 sub add_transactions_to_ap {
563 # go through all verified ap and acc_trans rows in import, adding acc_trans objects to ap objects
565 my $ap_entry; # the current ap row
567 foreach my $entry (@{ $self->controller->data }) {
568 # when we reach an ap_column for the first time, don't do anything, just store in $ap_entry
569 # when we reach an ap_column for the second time, save it
570 if ($entry->{raw_data}->{datatype} eq $self->_ap_column) {
571 if ( $ap_entry && $ap_entry->{object} ) { # won't trigger the first time, finishes the last object
572 if ( $ap_entry->{object}->{apchart} && $ap_entry->{object}->{apchart}->isa('SL::DB::Chart') ) {
573 $ap_entry->{object}->recalculate_amounts; # determine and set amount and netamount for ap
574 $ap_entry->{object}->create_ap_row(chart => $ap_entry->{object}->{apchart});
575 $ap_entry->{info_data}->{calc_amount} = $ap_entry->{object}->amount_as_number;
576 $ap_entry->{info_data}->{calc_netamount} = $ap_entry->{object}->netamount_as_number;
578 push @{ $ap_entry->{errors} }, $::locale->text("The payable chart isn't a valid chart.");
581 $ap_entry = $entry; # remember as last ap_entry
583 } elsif ( defined $ap_entry && $entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
584 push @{ $entry->{errors} }, $::locale->text('no tax_id in acc_trans') if !defined $entry->{object}->tax_id;
585 next if @{ $entry->{errors} };
587 my $acc_trans_objects = $ap_entry->{object}->add_ap_amount_row(
588 amount => $entry->{object}->amount,
589 chart => SL::DB::Manager::Chart->find_by(id => $entry->{object}->chart_id), # add_ap_amount takes chart obj. as argument
590 tax_id => $entry->{object}->tax_id,
591 project_id => $entry->{object}->project_id,
596 die "This should never happen\n";
600 # finish the final object
601 if ( $ap_entry->{object} ) {
602 if ( $ap_entry->{object}->{apchart} && $ap_entry->{object}->{apchart}->isa('SL::DB::Chart') ) {
603 $ap_entry->{object}->recalculate_amounts;
604 $ap_entry->{info_data}->{calc_amount} = $ap_entry->{object}->amount_as_number;
605 $ap_entry->{info_data}->{calc_netamount} = $ap_entry->{object}->netamount_as_number;
607 $ap_entry->{object}->create_ap_row(chart => $ap_entry->{object}->{apchart});
609 push @{ $ap_entry->{errors} }, $::locale->text("The payable chart isn't a valid chart.");
613 die "There was no final ap_entry object";
618 my ($self, %params) = @_;
620 # save all the Invoice objects
622 foreach my $entry (@{ $self->controller->data }) {
623 # only push the invoice objects that don't have an error
624 next if $entry->{raw_data}->{datatype} ne $self->_ap_column;
625 next if @{ $entry->{errors} };
627 die unless $entry->{object}->validate_acc_trans;
629 push @{ $objects_to_save }, $entry;
632 $self->SUPER::save_objects(data => $objects_to_save);
636 $_[0]->settings->{'ap_column'}
639 sub _transaction_column {
640 $_[0]->settings->{'transaction_column'}