a235bd9db01901e0d41db99abd112c357845bd23
[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
133   my $ct        = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
134   my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
135
136   my $tax = defined($params{tax_id})        ? SL::DB::Manager::Tax->find_by(id => $params{tax_id}) # 1. user param
137           : ref $chart_tax eq 'SL::DB::Tax' ? $chart_tax                                           # automatic tax
138           : SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);                              # no tax
139
140   die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
141
142   if ( $tax and $tax->rate != 0 ) {
143     ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
144   } else {
145     $netamount = $amount;
146   };
147
148   if ( $params{debit} ) {
149     $amount    *= -1;
150     $netamount *= -1;
151     $taxamount *= -1;
152   };
153
154   next unless $netamount; # skip entries with netamount 0
155
156   # initialise transactions if it doesn't exist yet
157   $self->transactions([]) unless $self->transactions;
158
159   require SL::DB::AccTransaction;
160   $self->add_transactions( SL::DB::AccTransaction->new(
161     chart_id       => $chart->id,
162     chart_link     => $chart->link,
163     amount         => $netamount,
164     taxkey         => $tax->taxkey,
165     tax_id         => $tax->id,
166     transdate      => $self->transdate,
167     source         => $params{source} // '',
168     memo           => $params{memo}   // '',
169     ob_transaction => $self->ob_transaction,
170     cb_transaction => $self->cb_transaction,
171     project_id     => $params{project_id},
172   ));
173
174   # only add tax entry if amount is >= 0.01, defaults to 2 decimals
175   if ( $::form->round_amount(abs($taxamount), $dec) > 0 ) {
176     my $tax_chart = $tax->chart;
177     if ( $tax->chart ) {
178       $self->add_transactions(SL::DB::AccTransaction->new(
179                                 chart_id       => $tax_chart->id,
180                                 chart_link     => $tax_chart->link,
181                                 amount         => $taxamount,
182                                 taxkey         => $tax->taxkey,
183                                 tax_id         => $tax->id,
184                                 transdate      => $self->transdate,
185                                 ob_transaction => $self->ob_transaction,
186                                 cb_transaction => $self->cb_transaction,
187                                 source         => $params{source} // '',
188                                 memo           => $params{memo}   // '',
189                                 project_id     => $params{project_id},
190                               ));
191     };
192   };
193   return $self;
194 };
195
196 sub validate {
197   my ($self) = @_;
198
199   my @errors;
200
201   if ( $self->transactions && scalar @{ $self->transactions } ) {
202     my $debit_count  = map { $_->amount } grep { $_->amount > 0 } @{ $self->transactions };
203     my $credit_count = map { $_->amount } grep { $_->amount < 0 } @{ $self->transactions };
204
205     if ( $debit_count > 1 && $credit_count > 1 ) {
206       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. ' .
207                        'Due to known problems involving accounting software kivitendo does not allow these.');
208     } elsif ( $credit_count == 0 && $debit_count == 0 ) {
209       push @errors, t8('Booking needs at least one debit and one credit booking!');
210     } else {
211       # transactions formally ok, now check for out of balance:
212       my $sum = sum map { $_->amount } @{ $self->transactions };
213       # compare rounded amount to 0, to get around floating point problems, e.g.
214       # $sum = -2.77555756156289e-17
215       push @errors, t8('Out of balance transaction!') . $sum unless $::form->round_amount($sum,5) == 0;
216     };
217   } else {
218     push @errors, t8('Empty transaction!');
219   };
220
221   # fields enforced by interface
222   push @errors, t8('Reference missing!')   unless $self->reference;
223   push @errors, t8('Description missing!') unless $self->description;
224
225   # date checks
226   push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
227
228   if ( $self->transdate ) {
229     if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
230       if ( !$self->id ) {
231         push @errors, t8('Cannot post transaction for a closed period!')
232       } else {
233         push @errors, t8('Cannot change transaction in a closed period!')
234       };
235     };
236
237     push @errors, t8('Cannot post transaction above the maximum future booking date!')
238       if $::form->date_max_future($self->transdate, \%::myconfig);
239   }
240
241   return @errors;
242 }
243
244 1;
245
246 __END__
247
248 =pod
249
250 =encoding UTF-8
251
252 =head1 NAME
253
254 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
255
256 =head1 FUNCTIONS
257
258 =over 4
259
260 =item C<post>
261
262 Takes an unsaved but initialised GLTransaction object and saves it, but first
263 validates the object, sets certain defaults (e.g. employee), and then also runs
264 various checks, writes history, runs DATEV check, ...
265
266 Returns C<$self> on success and dies otherwise. The whole process is run inside
267 a transaction. If it fails then nothing is saved to or changed in the database.
268 A new transaction is only started if none are active.
269
270 Example of posting a GL transaction from scratch:
271
272   my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
273   my $gl_transaction = SL::DB::GLTransaction->new(
274     taxincluded => 1,
275     description => 'bar',
276     reference   => 'bla',
277     transdate   => DateTime->today_local,
278   )->add_chart_booking(
279     chart  => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
280     credit => 100,
281     tax_id => $tax_0->id,
282   )->add_chart_booking(
283     chart  => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
284     debit  => 100,
285     tax_id => $tax_0->id,
286   )->post;
287
288 =item C<add_chart_booking %params>
289
290 Adds an acc_trans entry to an existing GL transaction, depending on the tax it
291 will also automatically create the tax entry. The GL transaction already needs
292 to have certain values, e.g. transdate, taxincluded, ...
293 Tax can be either set via the param tax_id or it will be set automatically
294 depending on the chart configuration. If not set and no configuration is found
295 no tax entry will be created (taxkey 0).
296
297 Mandatory params are
298
299 =over 2
300
301 =item * chart as an RDBO object
302
303 =item * either debit OR credit (positive values)
304
305 =back
306
307 Optional params:
308
309 =over 2
310
311 =item * dec - number of decimals to round to, defaults to 2
312
313 =item * source
314
315 =item * memo
316
317 =item * project_id
318
319 =back
320
321 All other values are taken directly from the GL transaction.
322
323 For an example, see C<post>.
324
325 After adding an acc_trans entry the GL transaction shouldn't be modified (e.g.
326 values affecting the acc_trans entries, such as transdate or taxincluded
327 shouldn't be changed). There is currently no method for recalculating the
328 acc_trans entries after they were added.
329
330 Return C<$self>, so it allows chaining.
331
332 =item C<validate>
333
334 Runs various checks to see if the GL transaction is ready to be C<post>ed.
335
336 Will return an array of error strings if any necessary conditions aren't met.
337
338 =back
339
340 =head1 TODO
341
342 Nothing here yet.
343
344 =head1 AUTHOR
345
346 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
347 G. Richardson E<lt>grichardson@kivitec.deE<gt>
348
349 =cut