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;
132 my $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id})
133 // croak "Can't find tax with id " . $params{tax_id};
135 if ( $tax and $tax->rate != 0 ) {
136 ($netamount, $taxamount) = Form->calculate_tax($amount, $tax->rate, $self->taxincluded, $dec);
138 $netamount = $amount;
141 if ( $params{debit} ) {
147 next unless $netamount; # skip entries with netamount 0
149 # initialise transactions if it doesn't exist yet
150 $self->transactions([]) unless $self->transactions;
152 require SL::DB::AccTransaction;
153 $self->add_transactions( SL::DB::AccTransaction->new(
154 chart_id => $chart->id,
155 chart_link => $chart->link,
156 amount => $netamount,
157 taxkey => $tax->taxkey,
159 transdate => $self->transdate,
160 source => $params{source} // '',
161 memo => $params{memo} // '',
162 ob_transaction => $self->ob_transaction,
163 cb_transaction => $self->cb_transaction,
164 project_id => $params{project_id},
167 # only add tax entry if amount is >= 0.01, defaults to 2 decimals
168 if ( $::form->round_amount(abs($taxamount), $dec) > 0 ) {
169 my $tax_chart = $tax->chart;
171 $self->add_transactions(SL::DB::AccTransaction->new(
172 chart_id => $tax_chart->id,
173 chart_link => $tax_chart->link,
174 amount => $taxamount,
175 taxkey => $tax->taxkey,
177 transdate => $self->transdate,
178 ob_transaction => $self->ob_transaction,
179 cb_transaction => $self->cb_transaction,
180 source => $params{source} // '',
181 memo => $params{memo} // '',
182 project_id => $params{project_id},
194 if ( $self->transactions && scalar @{ $self->transactions } ) {
195 my $debit_count = map { $_->amount } grep { $_->amount > 0 } @{ $self->transactions };
196 my $credit_count = map { $_->amount } grep { $_->amount < 0 } @{ $self->transactions };
198 if ( $debit_count > 1 && $credit_count > 1 ) {
199 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. ' .
200 'Due to known problems involving accounting software kivitendo does not allow these.');
201 } elsif ( $credit_count == 0 && $debit_count == 0 ) {
202 push @errors, t8('Booking needs at least one debit and one credit booking!');
204 # transactions formally ok, now check for out of balance:
205 my $sum = sum map { $_->amount } @{ $self->transactions };
206 # compare rounded amount to 0, to get around floating point problems, e.g.
207 # $sum = -2.77555756156289e-17
208 push @errors, t8('Out of balance transaction!') unless $::form->round_amount($sum,5) == 0;
211 push @errors, t8('Empty transaction!');
214 # fields enforced by interface
215 push @errors, t8('Reference missing!') unless $self->reference;
216 push @errors, t8('Description missing!') unless $self->description;
219 push @errors, t8('Transaction Date missing!') unless $self->transdate && ref($self->transdate) eq 'DateTime';
221 if ( $self->transdate ) {
222 if ( $::form->date_closed( $self->transdate, \%::myconfig) ) {
224 push @errors, t8('Cannot post transaction for a closed period!')
226 push @errors, t8('Cannot change transaction in a closed period!')
230 push @errors, t8('Cannot post transaction above the maximum future booking date!')
231 if $::form->date_max_future($self->transdate, \%::myconfig);
247 SL::DB::GLTransaction: Rose model for GL transactions (table "gl")
255 Takes an unsaved but initialised GLTransaction object and saves it, but first
256 validates the object, sets certain defaults (e.g. employee), and then also runs
257 various checks, writes history, runs DATEV check, ...
259 Returns C<$self> on success and dies otherwise. The whole process is run inside
260 a transaction. If it fails then nothing is saved to or changed in the database.
261 A new transaction is only started if none are active.
263 Example of posting a GL transaction from scratch:
265 my $tax_0 = SL::DB::Manager::Tax->find_by(taxkey => 0, rate => 0.00);
266 my $gl_transaction = SL::DB::GLTransaction->new(
268 description => 'bar',
270 transdate => DateTime->today_local,
271 )->add_chart_booking(
272 chart => SL::DB::Manager::Chart->find_by( description => 'Kasse' ),
274 tax_id => $tax_0->id,
275 )->add_chart_booking(
276 chart => SL::DB::Manager::Chart->find_by( description => 'Bank' ),
278 tax_id => $tax_0->id,
281 =item C<add_chart_booking %params>
283 Adds an acc_trans entry to an existing GL transaction, depending on the tax it
284 will also automatically create the tax entry. The GL transaction already needs
285 to have certain values, e.g. transdate, taxincluded, ...
291 =item * chart as an RDBO object
295 =item * either debit OR credit (positive values)
303 =item * dec - number of decimals to round to, defaults to 2
313 All other values are taken directly from the GL transaction.
315 For an example, see C<post>.
317 After adding an acc_trans entry the GL transaction shouldn't be modified (e.g.
318 values affecting the acc_trans entries, such as transdate or taxincluded
319 shouldn't be changed). There is currently no method for recalculating the
320 acc_trans entries after they were added.
322 Return C<$self>, so it allows chaining.
326 Runs various checks to see if the GL transaction is ready to be C<post>ed.
328 Will return an array of error strings if any necessary conditions aren't met.
338 Moritz Bunkus E<lt>m.bunkus@linet-services.deE<gt>,
339 G. Richardson E<lt>grichardson@kivitec.deE<gt>