1 package SL::Controller::YearEndTransactions;
5 use parent qw(SL::Controller::Base);
7 use utf8; # Umlauts in hardcoded German default texts
9 use SL::Locale::String qw(t8);
10 use SL::Helper::Flash;
13 use List::Util qw(sum);
17 use SL::DB::GLTransaction;
18 use SL::DB::AccTransaction;
20 use SL::DB::Helper::AccountingPeriod qw(get_balance_starting_date get_balance_startdate_method_options);
22 use Rose::Object::MakeMethods::Generic (
23 'scalar --get_set_init' => [ qw(cb_date cb_startdate ob_date) ],
26 __PACKAGE__->run_before('check_auth');
31 $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date)));
33 my $defaults = SL::DB::Default->get;
34 my $carry_over_chart = SL::DB::Manager::Chart->find_by( id => $defaults->carry_over_account_chart_id );
35 my $profit_chart = SL::DB::Manager::Chart->find_by( id => $defaults->profit_carried_forward_chart_id );
36 my $loss_chart = SL::DB::Manager::Chart->find_by( id => $defaults->loss_carried_forward_chart_id );
38 $self->render('yearend/form',
39 title => t8('Year-end closing'),
40 carry_over_chart => $carry_over_chart,
41 profit_chart => $profit_chart,
42 loss_chart => $loss_chart,
43 balance_startdate_method_options => get_balance_startdate_method_options(),
47 sub action_year_end_bookings {
53 _year_end_bookings( start_date => $self->cb_startdate,
54 cb_date => $self->cb_date,
58 $self->js->flash('error', t8('Error while applying year-end bookings!') . ' ' . $@);
59 return $self->js->render;
62 my ($report_data, $profit_loss_sum) = _report(
63 cb_date => $self->cb_date,
64 start_date => $self->cb_startdate,
67 my $html = $self->render('yearend/_charts', { layout => 0 , process => 1, output => 0 },
68 charts => $report_data,
69 profit_loss_sum => $profit_loss_sum,
71 return $self->js->flash('info', t8('Year-end bookings were successfully completed!'))
72 ->html('#charts', $html)
76 sub action_get_start_date {
79 my $cb_date = $self->cb_date; # parse from form via init
80 unless ( $self->cb_date ) {
81 return $self->hide('#apply_year_end_bookings_button')
82 ->flash('error', t8('Year-end date missing'))
86 $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date, $::form->{'balance_startdate_method'})));
88 # $main::lxdebug->message(0, "found start date: ", $self->cb_startdate->to_kivitendo);
90 return $self->js->val('#cb_startdate', $self->cb_startdate->to_kivitendo)
91 ->show('#apply_year_end_bookings_button')
96 sub action_update_charts {
101 my ($report_data, $profit_loss_sum) = _report(
102 cb_date => $self->cb_date,
103 start_date => $self->cb_startdate,
106 $self->render('yearend/_charts', { layout => 0 , process => 1 },
107 charts => $report_data,
108 profit_loss_sum => $profit_loss_sum,
120 $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date)));
122 die "cb_date must come after start_date" unless $self->cb_date > $self->cb_startdate;
125 sub _year_end_bookings {
128 my $start_date = delete $params{start_date};
129 my $cb_date = delete $params{cb_date};
131 my $defaults = SL::DB::Default->get;
132 my $carry_over_chart = SL::DB::Manager::Chart->find_by( id => $defaults->carry_over_account_chart_id ) // die t8('No carry-over chart configured!');
133 my $profit_chart = SL::DB::Manager::Chart->find_by( id => $defaults->profit_carried_forward_chart_id ) // die t8('No profit carried forward chart configured!');
134 my $loss_chart = SL::DB::Manager::Chart->find_by( id => $defaults->loss_carried_forward_chart_id ) // die t8('No profit and loss carried forward chart configured!');
136 my ($report_data, $profit_loss_sum) = _report(
137 start_date => $start_date,
141 # load all charts from report as objects and store them in a hash
142 my @report_chart_ids = map { $_->{chart_id} } @{ $report_data };
143 my %charts_by_id = map { ( $_->id => $_ ) } @{ SL::DB::Manager::Chart->get_all(where => [ id => \@report_chart_ids ]) };
145 my @asset_accounts = grep { $_->{account_type} eq 'asset_account' } @{ $report_data };
146 my @profit_loss_accounts = grep { $_->{account_type} eq 'profit_loss_account' } @{ $report_data };
148 my $ob_date = $cb_date->clone->add(days => 1);
150 my ($credit_sum, $debit_sum) = (0,0);
152 my $employee_id = SL::DB::Manager::Employee->current->id;
154 # rather than having one gl transaction for each asset account, we group all
155 # the debit sums and credit sums for cb and ob bookings, so we will have 4 gl
163 my $db = SL::DB->client;
164 $db->with_transaction(sub {
166 ######### asset accounts ########
167 # need cb and ob transactions
169 my $debit_balance = 0;
170 my $credit_balance = 0;
172 my $asset_cb_debit_entry = SL::DB::GLTransaction->new(
173 employee_id => $employee_id,
174 transdate => $cb_date,
175 reference => 'SB ' . $cb_date->year,
176 description => 'Automatische SB-Buchungen Bestandskonten Soll für ' . $cb_date->year,
182 my $asset_ob_debit_entry = SL::DB::GLTransaction->new(
183 employee_id => $employee_id,
184 transdate => $ob_date,
185 reference => 'EB ' . $ob_date->year,
186 description => 'Automatische EB-Buchungen Bestandskonten Haben für ' . $ob_date->year,
192 my $asset_cb_credit_entry = SL::DB::GLTransaction->new(
193 employee_id => $employee_id,
194 transdate => $cb_date,
195 reference => 'SB ' . $cb_date->year,
196 description => 'Automatische SB-Buchungen Bestandskonten Haben für ' . $cb_date->year,
202 my $asset_ob_credit_entry = SL::DB::GLTransaction->new(
203 employee_id => $employee_id,
204 transdate => $ob_date,
205 reference => 'EB ' . $ob_date->year,
206 description => 'Automatische EB-Buchungen Bestandskonten Soll für ' . $ob_date->year,
213 foreach my $asset_account ( @asset_accounts ) {
214 next if $asset_account->{amount_with_cb} == 0;
215 my $ass_acc = $charts_by_id{ $asset_account->{chart_id} };
217 if ( $asset_account->{amount_with_cb} < 0 ) {
218 # $main::lxdebug->message(0, sprintf("adding accno %s with balance %s to debit", $asset_account->{accno}, $asset_account->{amount_with_cb}));
219 $debit_balance += $asset_account->{amount_with_cb};
221 $asset_cb_debit_entry->add_chart_booking(
223 credit => - $asset_account->{amount_with_cb},
226 $asset_ob_debit_entry->add_chart_booking(
228 debit => - $asset_account->{amount_with_cb},
233 # $main::lxdebug->message(0, sprintf("adding accno %s with balance %s to credit", $asset_account->{accno}, $asset_account->{amount_with_cb}));
234 $credit_balance += $asset_account->{amount_with_cb};
236 $asset_cb_credit_entry->add_chart_booking(
238 debit => $asset_account->{amount_with_cb},
241 $asset_ob_credit_entry->add_chart_booking(
243 credit => $asset_account->{amount_with_cb},
249 if ( $debit_balance ) {
250 $asset_cb_debit_entry->add_chart_booking(
251 chart => $carry_over_chart,
252 debit => -1 * $debit_balance,
256 $asset_ob_debit_entry->add_chart_booking(
257 chart => $carry_over_chart,
258 credit => -1 * $debit_balance,
263 if ( $credit_balance ) {
264 $asset_cb_credit_entry->add_chart_booking(
265 chart => $carry_over_chart,
266 credit => $credit_balance,
269 $asset_ob_credit_entry->add_chart_booking(
270 chart => $carry_over_chart,
271 debit => $credit_balance,
276 $asset_cb_debit_entry->post if scalar @{ $asset_cb_debit_entry->transactions } > 1;
277 $asset_ob_debit_entry->post if scalar @{ $asset_ob_debit_entry->transactions } > 1;
278 $asset_cb_credit_entry->post if scalar @{ $asset_cb_credit_entry->transactions } > 1;
279 $asset_ob_credit_entry->post if scalar @{ $asset_ob_credit_entry->transactions } > 1;
281 ####### profit-loss accounts #######
282 # these only have a closing balance, the balance is transferred to the profit-loss account
284 # need to know if profit or loss first!
285 # use amount_with_cb, so it can be run several times. So sum may be 0 the second time.
286 my $profit_loss_sum = sum map { $_->{amount_with_cb} }
287 grep { $_->{account_type} eq 'profit_loss_account' }
289 $profit_loss_sum ||= 0;
291 if ( $profit_loss_sum > 0 ) {
292 $pl_chart = $profit_chart;
294 $pl_chart = $loss_chart;
297 my $pl_debit_balance = 0;
298 my $pl_credit_balance = 0;
299 # soll = debit, haben = credit
300 my $pl_cb_debit_entry = SL::DB::GLTransaction->new(
301 employee_id => $employee_id,
302 transdate => $cb_date,
303 reference => 'SB ' . $cb_date->year,
304 description => 'Automatische SB-Buchungen Erfolgskonten Soll für ' . $cb_date->year,
310 my $pl_cb_credit_entry = SL::DB::GLTransaction->new(
311 employee_id => $employee_id,
312 transdate => $cb_date,
313 reference => 'SB ' . $cb_date->year,
314 description => 'Automatische SB-Buchungen Erfolgskonten Haben für ' . $cb_date->year,
321 foreach my $profit_loss_account ( @profit_loss_accounts ) {
322 # $main::lxdebug->message(0, sprintf("found chart %s with balance %s", $profit_loss_account->{accno}, $profit_loss_account->{amount_with_cb}));
323 my $chart = $charts_by_id{ $profit_loss_account->{chart_id} };
325 next if $profit_loss_account->{amount_with_cb} == 0;
327 if ( $profit_loss_account->{amount_with_cb} < 0 ) {
328 $pl_debit_balance -= $profit_loss_account->{amount_with_cb};
329 $pl_cb_debit_entry->add_chart_booking(
332 credit => - $profit_loss_account->{amount_with_cb},
335 $pl_credit_balance += $profit_loss_account->{amount_with_cb};
336 $pl_cb_credit_entry->add_chart_booking(
339 debit => $profit_loss_account->{amount_with_cb},
344 # $main::lxdebug->message(0, "pl_debit_balance = $pl_debit_balance");
345 # $main::lxdebug->message(0, "pl_credit_balance = $pl_credit_balance");
347 $pl_cb_debit_entry->add_chart_booking(
350 debit => $pl_debit_balance,
351 ) if $pl_debit_balance;
353 $pl_cb_credit_entry->add_chart_booking(
356 credit => $pl_credit_balance,
357 ) if $pl_credit_balance;
359 # printf("debit : %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $pl_cb_debit_entry->transactions };
360 # printf("credit: %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $pl_cb_credit_entry->transactions };
362 $pl_cb_debit_entry->post if scalar @{ $pl_cb_debit_entry->transactions } > 1;
363 $pl_cb_credit_entry->post if scalar @{ $pl_cb_credit_entry->transactions } > 1;
365 ######### profit-loss transfer #########
366 # and finally transfer the new balance of the profit-loss account via the carry-over account
367 # we want to use profit_loss_sum with cb!
369 if ( $profit_loss_sum != 0 ) {
371 my $carry_over_cb_entry = SL::DB::GLTransaction->new(
372 employee_id => $employee_id,
373 transdate => $cb_date,
374 reference => 'SB ' . $cb_date->year,
375 description => sprintf('Automatische SB-Buchung für %s %s',
376 $profit_loss_sum >= 0 ? 'Gewinnvortrag' : 'Verlustvortrag',
384 my $carry_over_ob_entry = SL::DB::GLTransaction->new(
385 employee_id => $employee_id,
386 transdate => $ob_date,
387 reference => 'EB ' . $ob_date->year,
388 description => sprintf('Automatische EB-Buchung für %s %s',
389 $profit_loss_sum >= 0 ? 'Gewinnvortrag' : 'Verlustvortrag',
398 my ($amount1, $amount2);
399 if ( $profit_loss_sum < 0 ) {
407 $carry_over_cb_entry->add_chart_booking(
408 chart => $carry_over_chart,
410 $amount1 => abs($profit_loss_sum),
412 $carry_over_cb_entry->add_chart_booking(
415 $amount2 => abs($profit_loss_sum),
417 $carry_over_ob_entry->add_chart_booking(
418 chart => $carry_over_chart,
420 $amount2 => abs($profit_loss_sum),
422 $carry_over_ob_entry->add_chart_booking(
425 $amount1 => abs($profit_loss_sum),
428 # printf("debit : %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $carry_over_ob_entry->transactions };
429 # printf("credit: %s -> %s\n", $_->chart->displayable_name, $_->amount) foreach @{ $carry_over_ob_entry->transactions };
431 $carry_over_cb_entry->post if scalar @{ $carry_over_cb_entry->transactions } > 1;
432 $carry_over_ob_entry->post if scalar @{ $carry_over_ob_entry->transactions } > 1;
435 my $consistency_query = <<SQL;
438 where (ob_transaction is true or cb_transaction is true)
439 and (transdate = ? or transdate = ?)
441 my ($sum) = selectrow_query($::form, $db->dbh, $consistency_query,
445 die "acc_trans transactions don't add up to zero" unless $sum == 0;
448 }) or die $db->error;
454 my $start_date = delete $params{start_date};
455 my $cb_date = delete $params{cb_date};
457 my $defaults = SL::DB::Default->get;
458 die "no carry over account defined"
459 unless defined $defaults->carry_over_account_chart_id
460 and $defaults->carry_over_account_chart_id > 0;
462 my $salden_query = <<SQL;
463 select c.id as chart_id,
467 sum(a.amount) filter (where cb_transaction is false and ob_transaction is false) as amount,
468 sum(a.amount) filter (where ob_transaction is true ) as ob_amount,
469 sum(a.amount) filter (where cb_transaction is false ) as amount_without_cb,
470 sum(a.amount) filter (where cb_transaction is true ) as cb_amount,
471 sum(a.amount) as amount_with_cb,
472 case when c.category = ANY( '{I,E}' ) then 'profit_loss_account'
473 when c.category = ANY( '{A,C,L,Q}' ) then 'asset_account'
477 inner join chart c on (c.id = a.chart_id)
478 where a.transdate >= ?
481 group by c.id, c.accno, c.category
482 order by account_type, c.accno
485 my $dbh = SL::DB->client->dbh;
486 my $report = selectall_hashref_query($::form, $dbh, $salden_query,
489 $defaults->carry_over_account_chart_id,
491 # profit_loss_sum is the actual profit/loss for the year, without cb, use "amount_without_cb")
492 my $profit_loss_sum = sum map { $_->{amount_without_cb} }
493 grep { $_->{account_type} eq 'profit_loss_account' }
496 return ($report, $profit_loss_sum);
504 $::auth->assert('general_ledger');
512 sub init_ob_date { $::locale->parse_date_to_object($::form->{ob_date}) }
513 sub init_cb_startdate { $::locale->parse_date_to_object($::form->{cb_startdate}) }
514 sub init_cb_date { $::locale->parse_date_to_object($::form->{cb_date}) }