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