1 package SL::DB::GLTransaction;
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);
14 __PACKAGE__->meta->add_relationship(
16 type => 'one to many',
17 class => 'SL::DB::AccTransaction',
18 column_map => { id => 'trans_id' },
20 with_objects => [ 'chart' ],
21 sort_by => 'acc_trans_id ASC',
26 __PACKAGE__->meta->initialize;
28 sub record_type {return 'gl_transaction';}
29 sub record_number {goto &id;}
30 sub displayable_name {goto &oneline_summary;}
35 my $abbreviation = $::locale->text('GL Transaction (abbreviation)');
36 $abbreviation .= "(" . $::locale->text('Storno (one letter abbreviation)') . ")" if $self->storno;
40 sub displayable_type {
41 return t8('GL Transaction');
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);
55 $html = $self->presenter->gl_transaction(display => 'inline');
61 return $_[0]->reference;
64 sub date { goto &gldate }
69 my @errors = $self->validate;
70 croak t8("Errors in GL transaction:") . "\n" . join("\n", @errors) . "\n" if scalar @errors;
72 # make sure all the defaults are set:
73 require SL::DB::Employee;
74 my $employee_id = SL::DB::Manager::Employee->current->id;
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;
82 $self->db->with_transaction(sub {
85 if ($::instance_conf->get_datev_check_on_gl_transaction) {
86 my $datev = SL::DATEV->new(
88 trans_id => $self->id,
91 $datev->generate_datev_data;
94 die join "\n", t8('DATEV check returned errors:'), $datev->errors;
98 require SL::DB::History;
100 trans_id => $self->id,
101 snumbers => 'gltransaction_' . $self->id,
102 employee_id => $employee_id,
103 addition => 'POSTED',
104 what_done => 'gl transaction',
108 }) or die t8("Error when saving: #1", $self->db->error);
113 sub add_chart_booking {
114 my ($self, %params) = @_;
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});
125 my $chart = $params{chart};
127 my $dec = delete $params{dec} // 2;
129 my ($netamount,$taxamount) = (0,0);
130 my $amount = $params{credit} // $params{debit}; # only one can exist
132 croak t8('You cannot use a negative amount with debit/credit!') if $amount < 0;
136 my $ct = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
137 my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
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
143 die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
145 if ( $tax and $tax->rate != 0 ) {
146 ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
148 $netamount = $amount;
151 if ( $params{debit} ) {
157 return unless $netamount; # skip entries with netamount 0
159 # initialise transactions if it doesn't exist yet
160 $self->transactions([]) unless $self->transactions;
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,
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},
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;
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,
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},
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 };
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!');
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;
221 push @errors, t8('Empty transaction!');
224 # fields enforced by interface
225 push @errors, t8('Reference missing!') unless $self->reference;
226 push @errors, t8('Description missing!') unless $self->description;
229 push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
231 if ( $self->transdate ) {
232 if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
234 push @errors, t8('Cannot post transaction for a closed period!')
236 push @errors, t8('Cannot change transaction in a closed period!')
240 push @errors, t8('Cannot post transaction above the maximum future booking date!')
241 if $::form->date_max_future($self->transdate, \%::myconfig);
257 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
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, ...
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.
273 Example of posting a GL transaction from scratch:
275 my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
276 my $gl_transaction = SL::DB::GLTransaction->new(
278 description => 'bar',
280 transdate => DateTime->today_local,
281 )->add_chart_booking(
282 chart => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
284 tax_id => $tax_0->id,
285 )->add_chart_booking(
286 chart => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
288 tax_id => $tax_0->id,
291 =item C<add_chart_booking %params>
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).
304 =item * chart as an RDBO object
306 =item * either debit OR credit (positive values)
314 =item * dec - number of decimals to round to, defaults to 2
324 All other values are taken directly from the GL transaction.
326 For an example, see C<post>.
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.
333 Return C<$self>, so it allows chaining.
337 Runs various checks to see if the GL transaction is ready to be C<post>ed.
339 Will return an array of error strings if any necessary conditions aren't met.
349 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
350 G. Richardson E<lt>grichardson@kivitec.deE<gt>