36760fe3887b4f8396205961b3ce184d2d78034f
[kivitendo-erp.git] / 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_gln',            description => $::locale->text('Customer GLN')                          },
112                                  { name => 'customer_id',             description => $::locale->text('Customer (database ID)')                },
113                                  { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
114                                  { name => 'language',                description => $::locale->text('Language (name)')                       },
115                                  { name => 'payment_id',              description => $::locale->text('Payment terms (database ID)')           },
116                                  { name => 'payment',                 description => $::locale->text('Payment terms (name)')                  },
117                                  { name => 'taxzone_id',              description => $::locale->text('Tax zone (database ID)')                },
118                                  { name => 'taxzone',                 description => $::locale->text('Tax zone (description)')                },
119                                  { name => 'department_id',           description => $::locale->text('Department (database ID)')              },
120                                  { name => 'department',              description => $::locale->text('Department (description)')              },
121                                  { name => 'globalproject_id',        description => $::locale->text('Document Project (database ID)')        },
122                                  { name => 'globalprojectnumber',     description => $::locale->text('Document Project (number)')             },
123                                  { name => 'globalproject',           description => $::locale->text('Document Project (description)')        },
124                                  { name => 'archart',                 description => $::locale->text('Receivables account (account number)')  },
125                                  { name => 'orddate',                 description => $::locale->text('Order Date')                            },
126                                  { name => 'ordnumber',               description => $::locale->text('Order Number')                          },
127                                  { name => 'quonumber',               description => $::locale->text('Quotation Number')                      },
128                                  { name => 'quodate',                 description => $::locale->text('Quotation Date')                        },
129                                 );
130
131   $self->add_displayable_columns($self->_transaction_column,
132                                  { name => 'datatype',      description => $self->_transaction_column . ' [1]'       },
133                                  { name => 'projectnumber', description => $::locale->text('Project (number)')       },
134                                  { name => 'project',       description => $::locale->text('Project (description)')  },
135                                  { name => 'amount',        description => $::locale->text('Amount')                 },
136                                  { name => 'accno',         description => $::locale->text('Account number')         },
137                                  { name => 'taxkey',        description => $::locale->text('Taxkey')                 },
138                                 );
139 }
140
141 sub init_taxkeys_by {
142   my ($self) = @_;
143
144   my $all_taxes = SL::DB::Manager::Tax->get_all;
145   return { map { $_->taxkey => $_->id } @{ $all_taxes } };
146 }
147
148
149 sub init_charts_by {
150   my ($self) = @_;
151
152   my $all_charts = SL::DB::Manager::Chart->get_all;
153   return { map { my $col = $_; ( $col => { map { ( $_->$col => $_ ) } @{ $all_charts } } ) } qw(id accno) };
154 }
155
156 sub check_objects {
157   my ($self) = @_;
158
159   $self->controller->track_progress(phase => 'building data', progress => 0);
160
161   my $i = 0;
162   my $num_data = scalar @{ $self->controller->data };
163   my $invoice_entry;
164
165   foreach my $entry (@{ $self->controller->data }) {
166     $self->controller->track_progress(progress => $i/$num_data * 100) if $i % 100 == 0;
167
168     if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
169       $self->handle_invoice($entry);
170       $invoice_entry = $entry;
171     } elsif ($entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
172       die "Cannot process transaction row without an invoice row" if !$invoice_entry;
173       $self->handle_transaction($entry, $invoice_entry);
174     } else {
175       die "unknown datatype";
176     };
177
178   } continue {
179     $i++;
180   } # finished data parsing
181
182   $self->add_transactions_to_ar(); # go through all data entries again, adding receivable entry to ar lines while calculating amount and netamount
183
184   foreach my $entry (@{ $self->controller->data }) {
185     next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
186     $self->check_verify_amounts($entry->{object});
187   };
188
189   foreach my $entry (@{ $self->controller->data }) {
190     next unless ($entry->{raw_data}->{datatype} eq $self->_ar_column);
191     unless ( $entry->{object}->validate_acc_trans ) {
192       push @{ $entry->{errors} }, $::locale->text('Error: ar transaction doesn\'t validate');
193     };
194   };
195
196   # add info columns that aren't directly part of the object to be imported
197   # but are always determined or should always be shown because they are mandatory
198   $self->add_info_columns($self->_ar_column,
199                           { header => $::locale->text('Customer/Vendor'),     method => 'vc_name'   },
200                           { header => $::locale->text('Receivables account'), method => 'archart'   },
201                           { header => $::locale->text('Amount'),              method => 'amount'    },
202                           { header => $::locale->text('Net amount'),          method => 'netamount' },
203                           { header => $::locale->text('Tax zone'),            method => 'taxzone'   });
204
205   # Adding info_header this way only works, if the first invoice $self->controller->data->[0]
206
207   # Todo: access via ->[0] ok? Better: search first order column and use this
208   $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};
209
210   $self->add_info_columns($self->_ar_column, { header => $::locale->text('Project Number'), method => 'globalprojectnumber' }) if $self->controller->data->[0]->{info_data}->{globalprojectnumber};
211
212   $self->add_columns($self->_ar_column,
213                      map { "${_}_id" } grep { exists $self->controller->data->[0]->{raw_data}->{$_} } qw(payment department globalproject taxzone cp currency));
214   $self->add_columns($self->_ar_column, 'globalproject_id') if exists $self->controller->data->[0]->{raw_data}->{globalprojectnumber};
215   $self->add_columns($self->_ar_column, 'notes')            if exists $self->controller->data->[0]->{raw_data}->{notes};
216
217   # Todo: access via ->[1] ok? Better: search first item column and use this
218   $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Chart'), method => 'accno' });
219   $self->add_columns($self->_transaction_column, 'amount');
220
221   $self->add_info_columns($self->_transaction_column, { header => $::locale->text('Project Number'), method => 'projectnumber' }) if $self->controller->data->[1]->{info_data}->{projectnumber};
222
223   # $self->add_columns($self->_transaction_column,
224   #                    map { "${_}_id" } grep { exists $self->controller->data->[1]->{raw_data}->{$_} } qw(project price_factor pricegroup));
225   # $self->add_columns($self->_transaction_column,
226   #                    map { "${_}_id" } grep { exists $self->controller->data->[2]->{raw_data}->{$_} } qw(project price_factor pricegroup));
227   # $self->add_columns($self->_transaction_column, 'project_id') if exists $self->controller->data->[1]->{raw_data}->{projectnumber};
228   # $self->add_columns($self->_transaction_column, 'taxkey') if exists $self->controller->data->[1]->{raw_data}->{taxkey};
229
230   # If invoice has errors, add error for acc_trans items
231   # If acc_trans item has an error, add an error to the invoice item
232   my $ar_entry;
233   foreach my $entry (@{ $self->controller->data }) {
234     # Search first order
235     if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
236       $ar_entry = $entry;
237     } elsif ( defined $ar_entry
238               && $entry->{raw_data}->{datatype} eq $self->_transaction_column
239               && scalar @{ $ar_entry->{errors} } > 0 ) {
240       push @{ $entry->{errors} }, $::locale->text('Error: invalid ar row for this transaction');
241     } elsif ( defined $ar_entry
242               && $entry->{raw_data}->{datatype} eq $self->_transaction_column
243               && scalar @{ $entry->{errors} } > 0 ) {
244       push @{ $ar_entry->{errors} }, $::locale->text('Error: invalid acc transactions for this ar row');
245     }
246   }
247 }
248
249 sub handle_invoice {
250
251   my ($self, $entry) = @_;
252
253   my $object = $entry->{object};
254
255   $object->transactions( [] ); # initialise transactions for ar object so methods work on unsaved transactions
256
257   my $vc_obj;
258   if (any { $entry->{raw_data}->{$_} } qw(customer customernumber customer_gln customer_id)) {
259     $self->check_vc($entry, 'customer_id');
260     # check_vc only sets customer_id, but we need vc_obj later for customer defaults
261     $vc_obj = SL::DB::Customer->new(id => $object->customer_id)->load if $object->customer_id;
262   } elsif (any { $entry->{raw_data}->{$_} } qw(vendor vendornumber vendor_gln vendor_id)) {
263     $self->check_vc($entry, 'vendor_id');
264     $vc_obj = SL::DB::Vendor->new(id => $object->vendor_id)->load if $object->vendor_id;
265   } else {
266     push @{ $entry->{errors} }, $::locale->text('Error: Customer/vendor missing');
267   }
268
269   # check for duplicate invnumbers already in database
270   if ( SL::DB::Manager::Invoice->get_all_count( where => [ invnumber => $object->invnumber ] ) ) {
271     push @{ $entry->{errors} }, $::locale->text('Error: invnumber already exists');
272   }
273
274   $self->check_archart($entry); # checks for receivable account
275   # $self->check_amounts($entry); # checks and sets amount and netamount, use verify_amount and verify_netamount instead
276   $self->check_payment($entry); # currency default from customer used below
277   $self->check_department($entry);
278   $self->check_taxincluded($entry);
279   $self->check_project($entry, global => 1);
280   $self->check_taxzone($entry); # taxzone default from customer used below
281   $self->check_currency($entry); # currency default from customer used below
282   $self->handle_salesman($entry);
283   $self->handle_employee($entry);
284
285   if ($vc_obj ) {
286     # copy defaults from customer if not specified in import file
287     foreach (qw(payment_id language_id taxzone_id currency_id)) {
288       $object->$_($vc_obj->$_) unless $object->$_;
289     }
290   }
291 }
292
293 sub check_taxkey {
294   my ($self, $entry, $invoice_entry, $chart) = @_;
295
296   die "check_taxkey needs chart object as an argument" unless ref($chart) eq 'SL::DB::Chart';
297   # problem: taxkey is not unique in table tax, normally one of those entries is chosen directly from a dropdown
298   # so we check if the chart has an active taxkey, and if it matches the taxkey from the import, use the active taxkey
299   # if the chart doesn't have an active taxkey, use the first entry from Tax that matches the taxkey
300
301   my $object         = $entry->{object};
302   my $invoice_object = $invoice_entry->{object};
303
304   unless ( defined $entry->{raw_data}->{taxkey} ) {
305     push @{ $entry->{errors} }, $::locale->text('Error: taxkey missing'); # don't just assume 0, force taxkey in import
306     return 0;
307   };
308
309   my $tax = $chart->get_active_taxkey($invoice_object->deliverydate // $invoice_object->transdate // DateTime->today_local)->tax;
310   if ( $entry->{raw_data}->{taxkey} != $tax->taxkey ) {
311    # assume there is only one tax entry with that taxkey, can't guess
312     $tax = SL::DB::Manager::Tax->get_first( where => [ taxkey => $entry->{raw_data}->{taxkey} ]);
313   };
314
315   unless ( $tax ) {
316     push @{ $entry->{errors} }, $::locale->text('Error: invalid taxkey');
317     return 0;
318   };
319
320   $object->taxkey($tax->taxkey);
321   $object->tax_id($tax->id);
322   return 1;
323 };
324
325 sub check_amounts {
326   my ($self, $entry) = @_;
327   # currently not used in favour of verify_amount and verify_netamount
328
329   my $object = $entry->{object};
330
331   unless ($entry->{raw_data}->{amount} && $entry->{raw_data}->{netamount}) {
332     push @{ $entry->{errors} }, $::locale->text('Error: need amount and netamount');
333     return 0;
334   };
335   unless ($entry->{raw_data}->{amount} * 1 && $entry->{raw_data}->{netamount} * 1) {
336     push @{ $entry->{errors} }, $::locale->text('Error: amount and netamount need to be numeric');
337     return 0;
338   };
339
340   $object->amount( $entry->{raw_data}->{amount} );
341   $object->netamount( $entry->{raw_data}->{netamount} );
342 };
343
344 sub handle_transaction {
345   my ($self, $entry, $invoice_entry) = @_;
346
347   # Prepare acc_trans data. amount is dealt with in add_transactions_to_ar
348
349   my $object = $entry->{object};
350
351   $self->check_project($entry, global => 0);
352   if ( $self->check_chart($entry) ) {
353     my $chart_obj = SL::DB::Manager::Chart->find_by(id => $object->chart_id);
354
355     unless ( $chart_obj->link =~ /AR_amount/ ) {
356       push @{ $entry->{errors} }, $::locale->text('Error: chart isn\'t an ar_amount chart');
357       return 0;
358     };
359
360     if ( $self->check_taxkey($entry, $invoice_entry, $chart_obj) ) {
361       # do nothing, taxkey was assigned, just continue
362     } else {
363       # missing taxkey, don't do anything
364       return 0;
365     };
366   } else {
367     return 0;
368   };
369
370   # check whether taxkey and automatic taxkey match
371   # 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;
372
373   die "no taxkey for transaction object" unless $object->taxkey or $object->taxkey == 0;
374
375 }
376
377 sub check_chart {
378   my ($self, $entry) = @_;
379
380   my $object = $entry->{object};
381
382   if (any { $entry->{raw_data}->{$_} } qw(accno chart_id)) {
383
384     # Check whether or not chart ID is valid.
385     if ($object->chart_id && !$self->charts_by->{id}->{ $object->chart_id }) {
386       push @{ $entry->{errors} }, $::locale->text('Error: invalid chart_id');
387       return 0;
388     }
389
390     # Map number to ID if given.
391     if (!$object->chart_id && $entry->{raw_data}->{accno}) {
392       my $chart = $self->charts_by->{accno}->{ $entry->{raw_data}->{accno} };
393       if (!$chart) {
394         push @{ $entry->{errors} }, $::locale->text('Error: invalid chart (accno)');
395         return 0;
396       }
397
398       $object->chart_id($chart->id);
399     }
400
401     # Map description to ID if given.
402     if (!$object->chart_id && $entry->{raw_data}->{description}) {
403       my $chart = $self->charts_by->{description}->{ $entry->{raw_data}->{description} };
404       if (!$chart) {
405         push @{ $entry->{errors} }, $::locale->text('Error: invalid chart');
406         return 0;
407       }
408
409       $object->chart_id($chart->id);
410     }
411
412     if ($object->chart_id) {
413       # add account number to preview
414       $entry->{info_data}->{accno} = $self->charts_by->{id}->{ $object->chart_id }->accno;
415     } else {
416       push @{ $entry->{errors} }, $::locale->text('Error: chart not found');
417       return 0;
418     }
419   } else {
420     push @{ $entry->{errors} }, $::locale->text('Error: chart missing');
421     return 0;
422   }
423
424   return 1;
425 }
426
427 sub check_archart {
428   my ($self, $entry) = @_;
429
430   my $chart;
431
432   if ( $entry->{raw_data}->{archart} ) {
433     my $archart = $entry->{raw_data}->{archart};
434     $chart = SL::DB::Manager::Chart->find_by(accno => $archart);
435     unless ($chart) {
436       push @{ $entry->{errors} }, $::locale->text("Error: can't find ar chart with accno #1", $archart);
437       return 0;
438     };
439   } elsif ( $::instance_conf->get_ar_chart_id ) {
440     $chart = SL::DB::Manager::Chart->find_by(id => $::instance_conf->get_ar_chart_id);
441   } else {
442     push @{ $entry->{errors} }, $::locale->text("Error: neither archart passed, no default receivables chart configured");
443     return 0;
444   };
445
446   unless ($chart->link eq 'AR') {
447     push @{ $entry->{errors} }, $::locale->text('Error: archart isn\'t an AR chart');
448     return 0;
449   };
450
451   $entry->{info_data}->{archart} = $chart->accno;
452   $entry->{object}->{archart} = $chart;
453   return 1;
454 };
455
456 sub check_taxincluded {
457   my ($self, $entry) = @_;
458
459   my $object = $entry->{object};
460
461   if ( $entry->{raw_data}->{taxincluded} ) {
462     if ( $entry->{raw_data}->{taxincluded} eq 'f' or $entry->{raw_data}->{taxincluded} eq '0' ) {
463       $object->taxincluded('0');
464     } elsif ( $entry->{raw_data}->{taxincluded} eq 't' or $entry->{raw_data}->{taxincluded} eq '1' ) {
465       $object->taxincluded('1');
466     } else {
467       push @{ $entry->{errors} }, $::locale->text('Error: taxincluded has to be t or f');
468       return 0;
469     };
470   } else {
471     push @{ $entry->{errors} }, $::locale->text('Error: taxincluded wasn\'t set');
472     return 0;
473   };
474   return 1;
475 };
476
477 sub check_verify_amounts {
478   my ($self) = @_;
479
480   # If amounts are given, show calculated amounts as info and given amounts (verify_xxx).
481   # And throw an error if the differences are too big.
482   my @to_verify = ( { column      => 'amount',
483                       raw_column  => 'verify_amount',
484                       info_header => 'Calc. Amount',
485                       info_method => 'calc_amount',
486                       err_msg     => $::locale->text('Amounts differ too much'),
487                     },
488                     { column      => 'netamount',
489                       raw_column  => 'verify_netamount',
490                       info_header => 'Calc. Net amount',
491                       info_method => 'calc_netamount',
492                       err_msg     => $::locale->text('Net amounts differ too much'),
493                     } );
494
495   foreach my $tv (@to_verify) {
496     if (exists $self->controller->data->[0]->{raw_data}->{ $tv->{raw_column} }) {
497       $self->add_raw_data_columns($self->_ar_column, $tv->{raw_column});
498       $self->add_info_columns($self->_ar_column,
499                               { header => $::locale->text($tv->{info_header}), method => $tv->{info_method} });
500     }
501
502     # check differences
503     foreach my $entry (@{ $self->controller->data }) {
504       if ( @{ $entry->{errors} } ) {
505         push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
506         return 0;
507       };
508
509       if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
510         next if !$entry->{raw_data}->{ $tv->{raw_column} };
511         my $parsed_value = $::form->parse_amount(\%::myconfig, $entry->{raw_data}->{ $tv->{raw_column} });
512         # round $abs_diff, otherwise it might trigger for 0.020000000000021
513         my $abs_diff = $::form->round_amount(abs($entry->{object}->${ \$tv->{column} } - $parsed_value),2);
514         if ( $abs_diff > $self->settings->{'max_amount_diff'}) {
515           push @{ $entry->{errors} }, $::locale->text($tv->{err_msg});
516         }
517       }
518     }
519   }
520 };
521
522 sub add_transactions_to_ar {
523   my ($self) = @_;
524
525   # go through all verified ar and acc_trans rows in import, adding acc_trans objects to ar objects
526
527   my $ar_entry;  # the current ar row
528
529   foreach my $entry (@{ $self->controller->data }) {
530     # when we reach an ar_column for the first time, don't do anything, just store in $ar_entry
531     # when we reach an ar_column for the second time, save it
532     if ($entry->{raw_data}->{datatype} eq $self->_ar_column) {
533       if ( $ar_entry && $ar_entry->{object} ) { # won't trigger the first time, finishes the last object
534         if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
535           $ar_entry->{object}->recalculate_amounts; # determine and set amount and netamount for ar
536           $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
537           $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
538           $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
539         } else {
540           push @{ $entry->{errors} }, $::locale->text("ar_chart isn't a valid chart");
541         };
542       };
543       $ar_entry = $entry; # remember as last ar_entry
544
545     } elsif ( defined $ar_entry && $entry->{raw_data}->{datatype} eq $self->_transaction_column ) {
546       push @{ $entry->{errors} }, $::locale->text('no tax_id in acc_trans') if !defined $entry->{object}->tax_id;
547       next if @{ $entry->{errors} };
548
549       my $acc_trans_objects = $ar_entry->{object}->add_ar_amount_row(
550         amount      => $entry->{object}->amount,
551         chart       => SL::DB::Manager::Chart->find_by(id => $entry->{object}->chart_id), # add_ar_amount takes chart obj. as argument
552         tax_id      => $entry->{object}->tax_id,
553         project_id  => $entry->{object}->project_id,
554         debug       => 0,
555       );
556
557     } else {
558       die "This should never happen\n";
559     };
560   }
561
562   # finish the final object
563   if ( $ar_entry->{object} ) {
564     if ( $ar_entry->{object}->{archart} && $ar_entry->{object}->{archart}->isa('SL::DB::Chart') ) {
565       $ar_entry->{object}->recalculate_amounts;
566       $ar_entry->{info_data}->{amount}    = $ar_entry->{object}->amount;
567       $ar_entry->{info_data}->{netamount} = $ar_entry->{object}->netamount;
568
569       $ar_entry->{object}->create_ar_row(chart => $ar_entry->{object}->{archart});
570     } else {
571       push @{ $ar_entry->{errors} }, $::locale->text("The receivables chart isn't a valid chart.");
572       return 0;
573     };
574   } else {
575     die "There was no final ar_entry object";
576   };
577 }
578
579 sub save_objects {
580   my ($self, %params) = @_;
581
582   # save all the Invoice objects
583   my $objects_to_save;
584   foreach my $entry (@{ $self->controller->data }) {
585     # only push the invoice objects that don't have an error
586     next if $entry->{raw_data}->{datatype} ne $self->_ar_column;
587     next if @{ $entry->{errors} };
588
589     die unless $entry->{object}->validate_acc_trans;
590
591     push @{ $objects_to_save }, $entry;
592   }
593
594   $self->SUPER::save_objects(data => $objects_to_save);
595 }
596
597 sub _ar_column {
598   $_[0]->settings->{'ar_column'}
599 }
600
601 sub _transaction_column {
602   $_[0]->settings->{'transaction_column'}
603 }
604
605 1;