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