1 package SL::DB::GLTransaction;
5 use SL::DB::MetaSetup::GLTransaction;
6 use SL::Locale::String qw(t8);
7 use List::Util qw(sum);
12 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
13 __PACKAGE__->meta->make_manager_class;
15 __PACKAGE__->meta->add_relationship(
17 type => 'one to many',
18 class => 'SL::DB::AccTransaction',
19 column_map => { id => 'trans_id' },
21 with_objects => [ 'chart' ],
22 sort_by => 'acc_trans_id ASC',
27 __PACKAGE__->meta->initialize;
32 my $abbreviation = $::locale->text('GL Transaction (abbreviation)');
33 $abbreviation .= "(" . $::locale->text('Storno (one letter abbreviation)') . ")" if $self->storno;
37 sub displayable_type {
38 return t8('GL Transaction');
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);
52 $html = $self->presenter->gl_transaction(display => 'inline');
58 return $_[0]->reference;
61 sub date { goto &gldate }
66 my @errors = $self->validate;
67 croak t8("Errors in GL transaction:") . "\n" . join("\n", @errors) . "\n" if scalar @errors;
69 # make sure all the defaults are set:
70 require SL::DB::Employee;
71 my $employee_id = SL::DB::Manager::Employee->current->id;
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;
79 $self->db->with_transaction(sub {
82 if ($::instance_conf->get_datev_check_on_gl_transaction) {
83 my $datev = SL::DATEV->new(
85 trans_id => $self->id,
88 $datev->generate_datev_data;
91 die join "\n", t8('DATEV check returned errors:'), $datev->errors;
95 require SL::DB::History;
97 trans_id => $self->id,
98 snumbers => 'gltransaction_' . $self->id,
99 employee_id => $employee_id,
100 addition => 'POSTED',
101 what_done => 'gl transaction',
105 }) or die t8("Error when saving: #1", $self->db->error);
110 sub add_chart_booking {
111 my ($self, %params) = @_;
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});
122 my $chart = $params{chart};
124 my $dec = delete $params{dec} // 2;
126 my ($netamount,$taxamount) = (0,0);
127 my $amount = $params{credit} // $params{debit}; # only one can exist
129 croak t8('You cannot use a negative amount with debit/credit!') if $amount < 0;
133 my $ct = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
134 my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
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
140 die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
142 if ( $tax and $tax->rate != 0 ) {
143 ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
145 $netamount = $amount;
148 if ( $params{debit} ) {
154 next unless $netamount; # skip entries with netamount 0
156 # initialise transactions if it doesn't exist yet
157 $self->transactions([]) unless $self->transactions;
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,
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},
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;
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,
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},
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 };
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!');
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!') unless $::form->round_amount($sum,5) == 0;
218 push @errors, t8('Empty transaction!');
221 # fields enforced by interface
222 push @errors, t8('Reference missing!') unless $self->reference;
223 push @errors, t8('Description missing!') unless $self->description;
226 push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
228 if ( $self->transdate ) {
229 if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
231 push @errors, t8('Cannot post transaction for a closed period!')
233 push @errors, t8('Cannot change transaction in a closed period!')
237 push @errors, t8('Cannot post transaction above the maximum future booking date!')
238 if $::form->date_max_future($self->transdate, \%::myconfig);
254 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
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, ...
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.
270 Example of posting a GL transaction from scratch:
272 my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
273 my $gl_transaction = SL::DB::GLTransaction->new(
275 description => 'bar',
277 transdate => DateTime->today_local,
278 )->add_chart_booking(
279 chart => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
281 tax_id => $tax_0->id,
282 )->add_chart_booking(
283 chart => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
285 tax_id => $tax_0->id,
288 =item C<add_chart_booking %params>
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).
301 =item * chart as an RDBO object
303 =item * either debit OR credit (positive values)
311 =item * dec - number of decimals to round to, defaults to 2
321 All other values are taken directly from the GL transaction.
323 For an example, see C<post>.
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.
330 Return C<$self>, so it allows chaining.
334 Runs various checks to see if the GL transaction is ready to be C<post>ed.
336 Will return an array of error strings if any necessary conditions aren't met.
346 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
347 G. Richardson E<lt>grichardson@kivitec.deE<gt>