1 package SL::DB::GLTransaction;
5 use SL::DB::Helper::LinkedRecords;
6 use SL::DB::MetaSetup::GLTransaction;
7 use SL::Locale::String qw(t8);
8 use List::Util qw(sum);
13 # Creates get_all, get_all_count, get_all_iterator, delete_all and update_all.
14 __PACKAGE__->meta->make_manager_class;
16 __PACKAGE__->meta->add_relationship(
18 type => 'one to many',
19 class => 'SL::DB::AccTransaction',
20 column_map => { id => 'trans_id' },
22 with_objects => [ 'chart' ],
23 sort_by => 'acc_trans_id ASC',
28 __PACKAGE__->meta->initialize;
33 my $abbreviation = $::locale->text('GL Transaction (abbreviation)');
34 $abbreviation .= "(" . $::locale->text('Storno (one letter abbreviation)') . ")" if $self->storno;
38 sub displayable_type {
39 return t8('GL Transaction');
44 my $amount = sum map { $_->amount if $_->amount > 0 } @{$self->transactions};
45 $amount = $::form->format_amount(\%::myconfig, $amount, 2);
46 return sprintf("%s: %s %s %s (%s)", $self->abbreviation, $self->description, $self->reference, $amount, $self->transdate->to_kivitendo);
53 $html = $self->presenter->gl_transaction(display => 'inline');
59 return $_[0]->reference;
62 sub date { goto &gldate }
67 my @errors = $self->validate;
68 croak t8("Errors in GL transaction:") . "\n" . join("\n", @errors) . "\n" if scalar @errors;
70 # make sure all the defaults are set:
71 require SL::DB::Employee;
72 my $employee_id = SL::DB::Manager::Employee->current->id;
74 $self->employee_id($employee_id) unless defined $self->employee_id || defined $self->employee;
75 $self->ob_transaction('f') unless defined $self->ob_transaction;
76 $self->cb_transaction('f') unless defined $self->cb_transaction;
77 $self->gldate(DateTime->today_local) unless defined $self->gldate; # should user even be allowed to set this manually?
78 $self->transdate(DateTime->today_local) unless defined $self->transdate;
80 $self->db->with_transaction(sub {
83 if ($::instance_conf->get_datev_check_on_gl_transaction) {
84 my $datev = SL::DATEV->new(
86 trans_id => $self->id,
89 $datev->generate_datev_data;
92 die join "\n", t8('DATEV check returned errors:'), $datev->errors;
96 require SL::DB::History;
98 trans_id => $self->id,
99 snumbers => 'gltransaction_' . $self->id,
100 employee_id => $employee_id,
101 addition => 'POSTED',
102 what_done => 'gl transaction',
106 }) or die t8("Error when saving: #1", $self->db->error);
111 sub add_chart_booking {
112 my ($self, %params) = @_;
114 require SL::DB::Chart;
115 die "add_chart_booking needs a transdate" unless $self->transdate;
116 die "add_chart_booking needs taxincluded" unless defined $self->taxincluded;
117 die "chart missing" unless $params{chart} && ref($params{chart}) eq 'SL::DB::Chart';
118 die t8('Booking needs at least one debit and one credit booking!')
119 unless $params{debit} or $params{credit}; # must exist and not be 0
120 die t8('Cannot have a value in both Debit and Credit!')
121 if defined($params{debit}) and defined($params{credit});
123 my $chart = $params{chart};
125 my $dec = delete $params{dec} // 2;
127 my ($netamount,$taxamount) = (0,0);
128 my $amount = $params{credit} // $params{debit}; # only one can exist
130 croak t8('You cannot use a negative amount with debit/credit!') if $amount < 0;
134 my $ct = $chart->get_active_taxkey($self->deliverydate // $self->transdate);
135 my $chart_tax = ref $ct eq 'SL::DB::TaxKey' ? $ct->tax : undef;
137 my $tax = defined($params{tax_id}) ? SL::DB::Manager::Tax->find_by(id => $params{tax_id}) # 1. user param
138 : ref $chart_tax eq 'SL::DB::Tax' ? $chart_tax # automatic tax
139 : SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00); # no tax
141 die "No valid tax found. User input:" . $params{tax_id} unless ref $tax eq 'SL::DB::Tax';
143 if ( $tax and $tax->rate != 0 ) {
144 ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
146 $netamount = $amount;
149 if ( $params{debit} ) {
155 next unless $netamount; # skip entries with netamount 0
157 # initialise transactions if it doesn't exist yet
158 $self->transactions([]) unless $self->transactions;
160 require SL::DB::AccTransaction;
161 $self->add_transactions( SL::DB::AccTransaction->new(
162 chart_id => $chart->id,
163 chart_link => $chart->link,
164 amount => $netamount,
165 taxkey => $tax->taxkey,
167 transdate => $self->transdate,
168 source => $params{source} // '',
169 memo => $params{memo} // '',
170 ob_transaction => $self->ob_transaction,
171 cb_transaction => $self->cb_transaction,
172 project_id => $params{project_id},
175 # only add tax entry if amount is >= 0.01, defaults to 2 decimals
176 if ( $::form->round_amount(abs($taxamount), $dec) > 0 ) {
177 my $tax_chart = $tax->chart;
179 $self->add_transactions(SL::DB::AccTransaction->new(
180 chart_id => $tax_chart->id,
181 chart_link => $tax_chart->link,
182 amount => $taxamount,
183 taxkey => $tax->taxkey,
185 transdate => $self->transdate,
186 ob_transaction => $self->ob_transaction,
187 cb_transaction => $self->cb_transaction,
188 source => $params{source} // '',
189 memo => $params{memo} // '',
190 project_id => $params{project_id},
202 if ( $self->transactions && scalar @{ $self->transactions } ) {
203 my $debit_count = map { $_->amount } grep { $_->amount > 0 } @{ $self->transactions };
204 my $credit_count = map { $_->amount } grep { $_->amount < 0 } @{ $self->transactions };
206 if ( $debit_count > 1 && $credit_count > 1 ) {
207 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. ' .
208 'Due to known problems involving accounting software kivitendo does not allow these.');
209 } elsif ( $credit_count == 0 && $debit_count == 0 ) {
210 push @errors, t8('Booking needs at least one debit and one credit booking!');
212 # transactions formally ok, now check for out of balance:
213 my $sum = sum map { $_->amount } @{ $self->transactions };
214 # compare rounded amount to 0, to get around floating point problems, e.g.
215 # $sum = -2.77555756156289e-17
216 push @errors, t8('Out of balance transaction!') . $sum unless $::form->round_amount($sum,5) == 0;
219 push @errors, t8('Empty transaction!');
222 # fields enforced by interface
223 push @errors, t8('Reference missing!') unless $self->reference;
224 push @errors, t8('Description missing!') unless $self->description;
227 push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
229 if ( $self->transdate ) {
230 if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
232 push @errors, t8('Cannot post transaction for a closed period!')
234 push @errors, t8('Cannot change transaction in a closed period!')
238 push @errors, t8('Cannot post transaction above the maximum future booking date!')
239 if $::form->date_max_future($self->transdate, \%::myconfig);
255 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
263 Takes an unsaved but initialised GLTransaction object and saves it, but first
264 validates the object, sets certain defaults (e.g. employee), and then also runs
265 various checks, writes history, runs DATEV check, ...
267 Returns C<$self> on success and dies otherwise. The whole process is run inside
268 a transaction. If it fails then nothing is saved to or changed in the database.
269 A new transaction is only started if none are active.
271 Example of posting a GL transaction from scratch:
273 my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
274 my $gl_transaction = SL::DB::GLTransaction->new(
276 description => 'bar',
278 transdate => DateTime->today_local,
279 )->add_chart_booking(
280 chart => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
282 tax_id => $tax_0->id,
283 )->add_chart_booking(
284 chart => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
286 tax_id => $tax_0->id,
289 =item C<add_chart_booking %params>
291 Adds an acc_trans entry to an existing GL transaction, depending on the tax it
292 will also automatically create the tax entry. The GL transaction already needs
293 to have certain values, e.g. transdate, taxincluded, ...
294 Tax can be either set via the param tax_id or it will be set automatically
295 depending on the chart configuration. If not set and no configuration is found
296 no tax entry will be created (taxkey 0).
302 =item * chart as an RDBO object
304 =item * either debit OR credit (positive values)
312 =item * dec - number of decimals to round to, defaults to 2
322 All other values are taken directly from the GL transaction.
324 For an example, see C<post>.
326 After adding an acc_trans entry the GL transaction shouldn't be modified (e.g.
327 values affecting the acc_trans entries, such as transdate or taxincluded
328 shouldn't be changed). There is currently no method for recalculating the
329 acc_trans entries after they were added.
331 Return C<$self>, so it allows chaining.
335 Runs various checks to see if the GL transaction is ready to be C<post>ed.
337 Will return an array of error strings if any necessary conditions aren't met.
347 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
348 G. Richardson E<lt>grichardson@kivitec.deE<gt>