1dbf2c813348d8d860497671b0309d1aab5bb4c9
[kivitendo-erp.git] / SL / DB / GLTransaction.pm
1 package SL::DB::GLTransaction;
2
3 use strict;
4
5 use SL::DB::Helper::LinkedRecords;
6 use SL::DB::MetaSetup::GLTransaction;
7 use SL::Locale::String qw(t8);
8 use List::Util qw(sum);
9 use SL::DATEV;
10 use Carp;
11 use Data::Dumper;
12
13 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
14 __PACKAGE__->meta->make_manager_class;
15
16 __PACKAGE__->meta->add_relationship(
17   transactions   => {
18     type         => 'one to many',
19     class        => 'SL::DB::AccTransaction',
20     column_map   => { id => 'trans_id' },
21     manager_args => {
22       with_objects => [ 'chart' ],
23       sort_by      => 'acc_trans_id ASC',
24     },
25   },
26 );
27
28 __PACKAGE__->meta->initialize;
29
30 sub abbreviation {
31   my $self = shift;
32
33   my $abbreviation = $::locale->text('GL Transaction (abbreviation)');
34   $abbreviation   .= "(" . $::locale->text('Storno (one letter abbreviation)') . ")" if $self->storno;
35   return $abbreviation;
36 }
37
38 sub displayable_type {
39   return t8('GL Transaction');
40 }
41
42 sub oneline_summary {
43   my ($self) = @_;
44   my $amount =  sum map { $_->amount if $_->amount > 0 } @{$self->transactions};
45   $amount = $::form->format_amount(\%::myconfig, $amount, 2);
46   return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->description, $self->reference, $amount, $self->transdate->to_kivitendo);
47 }
48
49 sub link {
50   my ($self) = @_;
51
52   my $html;
53   $html   = $self->presenter->gl_transaction(display => 'inline');
54
55   return $html;
56 }
57
58 sub invnumber {
59   return $_[0]->reference;
60 }
61
62 sub date { goto &gldate }
63
64 sub post {
65   my ($self) = @_;
66
67   my @errors = $self->validate;
68   croak t8("Errors in GL transaction:") . "\n" . join("\n", @errors) . "\n" if scalar @errors;
69
70   # make sure all the defaults are set:
71   require SL::DB::Employee;
72   my $employee_id = SL::DB::Manager::Employee->current->id;
73   $self->type(undef);
74   $self->employee_id($employee_id) unless defined $self->employee_id || defined $self->employee;
75   $self->ob_transaction('f') unless defined $self->ob_transaction;
76   $self->cb_transaction('f') unless defined $self->cb_transaction;
77   $self->gldate(DateTime->today_local) unless defined $self->gldate; # should user even be allowed to set this manually?
78   $self->transdate(DateTime->today_local) unless defined $self->transdate;
79
80   $self->db->with_transaction(sub {
81     $self->save;
82
83     if ($::instance_conf->get_datev_check_on_gl_transaction) {
84       my $datev = SL::DATEV->new(
85         dbh      => $self->dbh,
86         trans_id => $self->id,
87       );
88
89       $datev->generate_datev_data;
90
91       if ($datev->errors) {
92          die join "\n", t8('DATEV check returned errors:'), $datev->errors;
93       }
94     }
95
96     require SL::DB::History;
97     SL::DB::History->new(
98       trans_id    => $self->id,
99       snumbers    => 'gltransaction_' . $self->id,
100       employee_id => $employee_id,
101       addition    => 'POSTED',
102       what_done   => 'gl transaction',
103     )->save;
104
105     1;
106   }) or die t8("Error when saving: #1", $self->db->error);
107
108   return $self;
109 }
110
111 sub add_chart_booking {
112   my ($self, %params) = @_;
113
114   require SL::DB::Chart;
115   die "add_chart_booking needs a transdate" unless $self->transdate;
116   die "add_chart_booking needs taxincluded" unless defined $self->taxincluded;
117   die "chart missing" unless $params{chart} && ref($params{chart}) eq 'SL::DB::Chart';
118   die t8('Booking needs at least one debit and one credit booking!')
119     unless $params{debit} or $params{credit}; # must exist and not be 0
120   die t8('Cannot have a value in both Debit and Credit!')
121     if defined($params{debit}) and defined($params{credit});
122
123   my $chart = $params{chart};
124
125   my $dec = delete $params{dec} // 2;
126
127   my ($netamount,$taxamount) = (0,0);
128   my $amount = $params{credit} // $params{debit}; # only one can exist
129
130   croak t8('You cannot use a negative amount with debit/credit!') if $amount < 0;
131
132   require SL::DB::Tax;
133
134   my $ct        = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
135   my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
136
137   my $tax = defined($params{tax_id})        ? SL::DB::Manager::Tax->find_by(id => $params{tax_id}) # 1. user param
138           : ref $chart_tax eq 'SL::DB::Tax' ? $chart_tax                                           # automatic tax
139           : SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);                              # no tax
140
141   die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
142
143   if ( $tax and $tax->rate != 0 ) {
144     ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
145   } else {
146     $netamount = $amount;
147   };
148
149   if ( $params{debit} ) {
150     $amount    *= -1;
151     $netamount *= -1;
152     $taxamount *= -1;
153   };
154
155   next unless $netamount; # skip entries with netamount 0
156
157   # initialise transactions if it doesn't exist yet
158   $self->transactions([]) unless $self->transactions;
159
160   require SL::DB::AccTransaction;
161   $self->add_transactions( SL::DB::AccTransaction->new(
162     chart_id       => $chart->id,
163     chart_link     => $chart->link,
164     amount         => $netamount,
165     taxkey         => $tax->taxkey,
166     tax_id         => $tax->id,
167     transdate      => $self->transdate,
168     source         => $params{source} // '',
169     memo           => $params{memo}   // '',
170     ob_transaction => $self->ob_transaction,
171     cb_transaction => $self->cb_transaction,
172     project_id     => $params{project_id},
173   ));
174
175   # only add tax entry if amount is >= 0.01, defaults to 2 decimals
176   if ( $::form->round_amount(abs($taxamount), $dec) > 0 ) {
177     my $tax_chart = $tax->chart;
178     if ( $tax->chart ) {
179       $self->add_transactions(SL::DB::AccTransaction->new(
180                                 chart_id       => $tax_chart->id,
181                                 chart_link     => $tax_chart->link,
182                                 amount         => $taxamount,
183                                 taxkey         => $tax->taxkey,
184                                 tax_id         => $tax->id,
185                                 transdate      => $self->transdate,
186                                 ob_transaction => $self->ob_transaction,
187                                 cb_transaction => $self->cb_transaction,
188                                 source         => $params{source} // '',
189                                 memo           => $params{memo}   // '',
190                                 project_id     => $params{project_id},
191                               ));
192     };
193   };
194   return $self;
195 };
196
197 sub validate {
198   my ($self) = @_;
199
200   my @errors;
201
202   if ( $self->transactions && scalar @{ $self->transactions } ) {
203     my $debit_count  = map { $_->amount } grep { $_->amount > 0 } @{ $self->transactions };
204     my $credit_count = map { $_->amount } grep { $_->amount < 0 } @{ $self->transactions };
205
206     if ( $debit_count > 1 && $credit_count > 1 ) {
207       push @errors, t8('Split entry detected. The values you have entered will result in an entry with more than one position on both debit and credit. ' .
208                        'Due to known problems involving accounting software kivitendo does not allow these.');
209     } elsif ( $credit_count == 0 && $debit_count == 0 ) {
210       push @errors, t8('Booking needs at least one debit and one credit booking!');
211     } else {
212       # transactions formally ok, now check for out of balance:
213       my $sum = sum map { $_->amount } @{ $self->transactions };
214       # compare rounded amount to 0, to get around floating point problems, e.g.
215       # $sum = -2.77555756156289e-17
216       push @errors, t8('Out of balance transaction!') . $sum unless $::form->round_amount($sum,5) == 0;
217     };
218   } else {
219     push @errors, t8('Empty transaction!');
220   };
221
222   # fields enforced by interface
223   push @errors, t8('Reference missing!')   unless $self->reference;
224   push @errors, t8('Description missing!') unless $self->description;
225
226   # date checks
227   push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
228
229   if ( $self->transdate ) {
230     if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
231       if ( !$self->id ) {
232         push @errors, t8('Cannot post transaction for a closed period!')
233       } else {
234         push @errors, t8('Cannot change transaction in a closed period!')
235       };
236     };
237
238     push @errors, t8('Cannot post transaction above the maximum future booking date!')
239       if $::form->date_max_future($self->transdate, \%::myconfig);
240   }
241
242   return @errors;
243 }
244
245 1;
246
247 __END__
248
249 =pod
250
251 =encoding UTF-8
252
253 =head1 NAME
254
255 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
256
257 =head1 FUNCTIONS
258
259 =over 4
260
261 =item C<post>
262
263 Takes an unsaved but initialised GLTransaction object and saves it, but first
264 validates the object, sets certain defaults (e.g. employee), and then also runs
265 various checks, writes history, runs DATEV check, ...
266
267 Returns C<$self> on success and dies otherwise. The whole process is run inside
268 a transaction. If it fails then nothing is saved to or changed in the database.
269 A new transaction is only started if none are active.
270
271 Example of posting a GL transaction from scratch:
272
273   my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
274   my $gl_transaction = SL::DB::GLTransaction->new(
275     taxincluded => 1,
276     description => 'bar',
277     reference   => 'bla',
278     transdate   => DateTime->today_local,
279   )->add_chart_booking(
280     chart  => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
281     credit => 100,
282     tax_id => $tax_0->id,
283   )->add_chart_booking(
284     chart  => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
285     debit  => 100,
286     tax_id => $tax_0->id,
287   )->post;
288
289 =item C<add_chart_booking %params>
290
291 Adds an acc_trans entry to an existing GL transaction, depending on the tax it
292 will also automatically create the tax entry. The GL transaction already needs
293 to have certain values, e.g. transdate, taxincluded, ...
294 Tax can be either set via the param tax_id or it will be set automatically
295 depending on the chart configuration. If not set and no configuration is found
296 no tax entry will be created (taxkey 0).
297
298 Mandatory params are
299
300 =over 2
301
302 =item * chart as an RDBO object
303
304 =item * either debit OR credit (positive values)
305
306 =back
307
308 Optional params:
309
310 =over 2
311
312 =item * dec - number of decimals to round to, defaults to 2
313
314 =item * source
315
316 =item * memo
317
318 =item * project_id
319
320 =back
321
322 All other values are taken directly from the GL transaction.
323
324 For an example, see C<post>.
325
326 After adding an acc_trans entry the GL transaction shouldn't be modified (e.g.
327 values affecting the acc_trans entries, such as transdate or taxincluded
328 shouldn't be changed). There is currently no method for recalculating the
329 acc_trans entries after they were added.
330
331 Return C<$self>, so it allows chaining.
332
333 =item C<validate>
334
335 Runs various checks to see if the GL transaction is ready to be C<post>ed.
336
337 Will return an array of error strings if any necessary conditions aren't met.
338
339 =back
340
341 =head1 TODO
342
343 Nothing here yet.
344
345 =head1 AUTHOR
346
347 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
348 G. Richardson E<lt>grichardson@kivitec.deE<gt>
349
350 =cut