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>