5 use POSIX qw(strftime);
 
  10 use SL::DB::PurchaseInvoice;
 
  12 use SL::Locale::String qw(t8);
 
  16 sub retrieve_open_invoices {
 
  17   $main::lxdebug->enter_sub();
 
  22   my $myconfig = \%main::myconfig;
 
  23   my $form     = $main::form;
 
  25   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
  26   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
  27   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
  28   my $vc_vc_id = $params{vc} eq 'customer' ? 'c_vendor_id' : 'v_customer_id';
 
  30   my $mandate  = $params{vc} eq 'customer' ? " AND COALESCE(vc.mandator_id, '') <> '' AND vc.mandate_date_of_signature IS NOT NULL " : '';
 
  32   # in query: for customers, use payment terms from invoice, for vendors use
 
  33   # payment terms from vendor settings
 
  34   # currently there is no option in vendor invoices for setting payment terms,
 
  35   # so the vendor settings are always used
 
  37   my $payment_term_type = $params{vc} eq 'customer' ? "${arap}" : 'vc';
 
  39   # open_amount is not the current open amount according to bookkeeping, but
 
  40   # the open amount minus the SEPA transfer amounts that haven't been closed yet
 
  43        SELECT ${arap}.id, ${arap}.invnumber, ${arap}.transdate, ${arap}.${vc}_id as vc_id, ${arap}.amount AS invoice_amount, ${arap}.invoice,
 
  44          (${arap}.transdate + pt.terms_skonto) as skonto_date, (pt.percent_skonto * 100) as percent_skonto,
 
  45          (${arap}.amount - (${arap}.amount * pt.percent_skonto)) as amount_less_skonto,
 
  46          (${arap}.amount * pt.percent_skonto) as skonto_amount,
 
  47          vc.name AS vcname, vc.language_id, ${arap}.duedate as duedate, ${arap}.direct_debit,
 
  48          vc.${vc_vc_id} as vc_vc_id,
 
  50          COALESCE(vc.iban, '') <> '' AND COALESCE(vc.bic, '') <> '' ${mandate} AS vc_bank_info_ok,
 
  52          ${arap}.amount - ${arap}.paid - COALESCE(open_transfers.amount, 0) AS open_amount,
 
  53          COALESCE(open_transfers.amount, 0) AS transfer_amount,
 
  54          pt.description as pt_description
 
  57        LEFT JOIN ${vc} vc ON (${arap}.${vc}_id = vc.id)
 
  58        LEFT JOIN (SELECT sei.${arap}_id, SUM(sei.amount) + SUM(COALESCE(sei.skonto_amount,0)) AS amount
 
  59                   FROM sepa_export_items sei
 
  60                   LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id)
 
  63                   GROUP BY sei.${arap}_id)
 
  64          AS open_transfers ON (${arap}.id = open_transfers.${arap}_id)
 
  66        LEFT JOIN payment_terms pt ON (${payment_term_type}.payment_id = pt.id)
 
  68        WHERE ${arap}.amount > (COALESCE(open_transfers.amount, 0) + ${arap}.paid)
 
  70        ORDER BY lower(vc.name) ASC, lower(${arap}.invnumber) ASC
 
  72     #  $main::lxdebug->message(LXDebug->DEBUG2(),"sepa add query:".$query);
 
  74   my $results = selectall_hashref_query($form, $dbh, $query);
 
  76   # add some more data to $results:
 
  77   # create drop-down data for payment types and suggest amount to be paid according
 
  78   # to open amount or skonto
 
  80   foreach my $result ( @$results ) {
 
  81     my $invoice = $vc eq 'customer' ? SL::DB::Manager::Invoice->find_by(         id => $result->{id} )
 
  82                                     : SL::DB::Manager::PurchaseInvoice->find_by( id => $result->{id} );
 
  84     $invoice->get_payment_suggestions(sepa => 1); # consider amounts of open entries in sepa_export_items
 
  85     $result->{skonto_amount}             = $invoice->skonto_amount;
 
  86     $result->{within_skonto_period}      = $invoice->within_skonto_period;
 
  87     $result->{invoice_amount_suggestion} = $invoice->{invoice_amount_suggestion};
 
  88     $result->{payment_select_options}    = $invoice->{payment_select_options};
 
  91   $main::lxdebug->leave_sub();
 
  97   my ($self, %params) = @_;
 
  98   $main::lxdebug->enter_sub();
 
 100   my $rc = SL::DB->client->with_transaction(\&_create_export, $self, %params);
 
 102   $::lxdebug->leave_sub;
 
 110   Common::check_params(\%params, qw(employee bank_transfers vc));
 
 112   my $myconfig = \%main::myconfig;
 
 113   my $form     = $main::form;
 
 114   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 115   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 118   my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 120   my ($export_id) = selectfirst_array_query($form, $dbh, qq|SELECT nextval('sepa_export_id_seq')|);
 
 122     qq|INSERT INTO sepa_export (id, employee_id, vc)
 
 123        VALUES (?, (SELECT id
 
 125                    WHERE login = ?), ?)|;
 
 126   do_query($form, $dbh, $query, $export_id, $params{employee}, $vc);
 
 128   my $q_item_id = qq|SELECT nextval('id')|;
 
 129   my $h_item_id = prepare_query($form, $dbh, $q_item_id);
 
 130   my $c_mandate = $params{vc} eq 'customer' ? ', vc_mandator_id, vc_mandate_date_of_signature' : '';
 
 131   my $p_mandate = $params{vc} eq 'customer' ? ', ?, ?' : '';
 
 134     qq|INSERT INTO sepa_export_items (id,          sepa_export_id,           ${arap}_id,  chart_id,
 
 135                                       amount,      requested_execution_date, reference,   end_to_end_id,
 
 136                                       our_iban,    our_bic,                  vc_iban,     vc_bic,
 
 137                                       skonto_amount, payment_type ${c_mandate})
 
 142   my $h_insert = prepare_query($form, $dbh, $q_insert);
 
 145     qq|SELECT arap.invnumber,
 
 148           LEFT JOIN chart c ON (at.chart_id = c.id)
 
 149           WHERE (at.trans_id = ?)
 
 150             AND (c.link LIKE '%${ARAP}_paid%'))
 
 153           FROM sepa_export_items sei
 
 154           WHERE (sei.ap_id = ?))
 
 158   my $h_reference = prepare_query($form, $dbh, $q_reference);
 
 162   foreach my $transfer (@{ $params{bank_transfers} }) {
 
 163     if (!$transfer->{reference}) {
 
 164       do_statement($form, $h_reference, $q_reference, (conv_i($transfer->{"${arap}_id"})) x 3);
 
 166       my ($invnumber, $num_payments) = $h_reference->fetchrow_array();
 
 169       $transfer->{reference} = "${invnumber}-${num_payments}";
 
 172     $h_item_id->execute() || $::form->dberror($q_item_id);
 
 173     my ($item_id)      = $h_item_id->fetchrow_array();
 
 175     my $end_to_end_id  = strftime "LXO%Y%m%d%H%M%S", localtime;
 
 176     my $item_id_len    = length "$item_id";
 
 177     my $num_zeroes     = 35 - $item_id_len - length $end_to_end_id;
 
 178     $end_to_end_id    .= '0' x $num_zeroes if (0 < $num_zeroes);
 
 179     $end_to_end_id    .= $item_id;
 
 180     $end_to_end_id     = substr $end_to_end_id, 0, 35;
 
 182     my @values = ($item_id,                          $export_id,
 
 183                   conv_i($transfer->{"${arap}_id"}), conv_i($transfer->{chart_id}),
 
 184                   $transfer->{amount},               conv_date($transfer->{requested_execution_date}),
 
 185                   $transfer->{reference},            $end_to_end_id,
 
 186                   map { my $pfx = $_; map { $transfer->{"${pfx}_${_}"} } qw(iban bic) } qw(our vc));
 
 187     # save value of skonto_amount and payment_type
 
 188     if ( $transfer->{payment_type} eq 'without_skonto' ) {
 
 190     } elsif ($transfer->{payment_type} eq 'difference_as_skonto' ) {
 
 191       push(@values, $transfer->{amount});
 
 192     } elsif ($transfer->{payment_type} eq 'with_skonto_pt' ) {
 
 193       push(@values, $transfer->{skonto_amount});
 
 195       die "illegal payment_type: " . $transfer->{payment_type} . "\n";
 
 197     push(@values, $transfer->{payment_type});
 
 199     push @values, $transfer->{vc_mandator_id}, conv_date($transfer->{vc_mandate_date_of_signature}) if $params{vc} eq 'customer';
 
 201     do_statement($form, $h_insert, $q_insert, @values);
 
 205   $h_item_id->finish();
 
 210 sub retrieve_export {
 
 211   $main::lxdebug->enter_sub();
 
 216   Common::check_params(\%params, qw(id vc));
 
 218   my $myconfig = \%main::myconfig;
 
 219   my $form     = $main::form;
 
 220   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 221   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 223   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
 225   my ($joins, $columns);
 
 227   if ($params{details}) {
 
 228     $columns = ', arap.invoice';
 
 229     $joins   = "LEFT JOIN ${arap} arap ON (se.${arap}_id = arap.id)";
 
 234          CASE WHEN COALESCE(e.name, '') <> '' THEN e.name ELSE e.login END AS employee
 
 236        LEFT JOIN employee e ON (se.employee_id = e.id)
 
 239   my $export = selectfirst_hashref_query($form, $dbh, $query, conv_i($params{id}));
 
 242     my ($columns, $joins);
 
 244     my $mandator_id = $params{vc} eq 'customer' ? ', mandator_id, mandate_date_of_signature' : '';
 
 246     if ($params{details}) {
 
 247       $columns = qq|, arap.invnumber, arap.invoice, arap.transdate AS reference_date, vc.name AS vc_name, vc.${vc}number AS vc_number, c.accno AS chart_accno, c.description AS chart_description ${mandator_id}|;
 
 248       $joins   = qq|LEFT JOIN ${arap} arap ON (sei.${arap}_id = arap.id)
 
 249                     LEFT JOIN ${vc} vc     ON (arap.${vc}_id  = vc.id)
 
 250                     LEFT JOIN chart c      ON (sei.chart_id   = c.id)|;
 
 253     $query = qq|SELECT sei.*
 
 255                 FROM sepa_export_items sei
 
 257                 WHERE sei.sepa_export_id = ?|;
 
 259     $export->{items} = selectall_hashref_query($form, $dbh, $query, conv_i($params{id}));
 
 262     $export->{items} = [];
 
 265   $main::lxdebug->leave_sub();
 
 271   $main::lxdebug->enter_sub();
 
 276   Common::check_params(\%params, qw(id));
 
 278   my $myconfig = \%main::myconfig;
 
 279   my $form     = $main::form;
 
 281   SL::DB->client->with_transaction(sub {
 
 282     my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 284     my @ids          = ref $params{id} eq 'ARRAY' ? @{ $params{id} } : ($params{id});
 
 285     my $placeholders = join ', ', ('?') x scalar @ids;
 
 286     my $query        = qq|UPDATE sepa_export SET closed = TRUE WHERE id IN ($placeholders)|;
 
 288     do_query($form, $dbh, $query, map { conv_i($_) } @ids);
 
 290   }) or do { die SL::DB->client->error };
 
 292   $main::lxdebug->leave_sub();
 
 296   $main::lxdebug->enter_sub();
 
 301   Common::check_params(\%params, qw(id));
 
 303   my $sepa_export = SL::DB::Manager::SepaExport->find_by(id => $params{id});
 
 305   croak "Not a valid SEPA Export id: $params{id}" unless $sepa_export;
 
 306   croak "Cannot undo closed exports."             if $sepa_export->closed;
 
 307   croak "Cannot undo executed exports."           if $sepa_export->executed;
 
 309   die "Could not undo $sepa_export->id" if !$sepa_export->delete();
 
 311   $main::lxdebug->leave_sub();
 
 315   $main::lxdebug->enter_sub();
 
 320   my $myconfig = \%main::myconfig;
 
 321   my $form     = $main::form;
 
 322   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 323   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 325   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
 328     'id'          => [ 'se.id',                ],
 
 329     'export_date' => [ 'se.itime',             ],
 
 330     'employee'    => [ 'e.name',      'se.id', ],
 
 331     'executed'    => [ 'se.executed', 'se.id', ],
 
 332     'closed'      => [ 'se.closed',   'se.id', ],
 
 335   my %sort_spec = create_sort_spec('defs' => \%sort_columns, 'default' => 'id', 'column' => $params{sortorder}, 'dir' => $params{sortdir});
 
 337   my (@where, @values, @where_sub, @values_sub, %joins_sub);
 
 339   my $filter = $params{filter} || { };
 
 341   foreach (qw(executed closed)) {
 
 342     push @where, $filter->{$_} ? "se.$_" : "NOT se.$_" if (exists $filter->{$_});
 
 345   my %operators = ('from' => '>=',
 
 348   foreach my $dir (qw(from to)) {
 
 349     next unless ($filter->{"export_date_${dir}"});
 
 350     push @where,  "se.itime $operators{$dir} ?::date";
 
 351     push @values, $filter->{"export_date_${dir}"};
 
 354   if ($filter->{invnumber}) {
 
 355     push @where_sub,  "arap.invnumber ILIKE ?";
 
 356     push @values_sub, like($filter->{invnumber});
 
 357     $joins_sub{$arap} = 1;
 
 360   if ($filter->{message_id}) {
 
 361     push @values, like($filter->{message_id});
 
 364         SELECT sepa_export_id
 
 365         FROM sepa_export_message_ids
 
 366         WHERE message_id ILIKE ?
 
 372     push @where_sub,  "vc.name ILIKE ?";
 
 373     push @values_sub, like($filter->{vc});
 
 374     $joins_sub{$arap} = 1;
 
 378   foreach my $type (qw(requested_execution execution)) {
 
 379     foreach my $dir (qw(from to)) {
 
 380       next unless ($filter->{"${type}_date_${dir}"});
 
 381       push @where_sub,  "(items.${type}_date IS NOT NULL) AND (items.${type}_date $operators{$dir} ?)";
 
 382       push @values_sub, $filter->{"${type}_date_${_}"};
 
 388     $joins_sub    .= " LEFT JOIN ${arap} arap ON (items.${arap}_id = arap.id)" if ($joins_sub{$arap});
 
 389     $joins_sub    .= " LEFT JOIN ${vc} vc      ON (arap.${vc}_id   = vc.id)"   if ($joins_sub{vc});
 
 391     my $where_sub  = join(' AND ', map { "(${_})" } @where_sub);
 
 393     my $query_sub  = qq|se.id IN (SELECT items.sepa_export_id
 
 394                                   FROM sepa_export_items items
 
 398     push @where,  $query_sub;
 
 399     push @values, @values_sub;
 
 402   push @where,  'se.vc = ?';
 
 405   my $where = @where ? ' WHERE ' . join(' AND ', map { "(${_})" } @where) : '';
 
 408     qq|SELECT se.id, se.employee_id, se.executed, se.closed, itime::date AS export_date,
 
 410           FROM sepa_export_items sei
 
 411           WHERE (sei.sepa_export_id = se.id)) AS num_invoices,
 
 412          (SELECT SUM(sei.amount)
 
 413           FROM sepa_export_items sei
 
 414           WHERE (sei.sepa_export_id = se.id)) AS sum_amounts,
 
 415          (SELECT string_agg(semi.message_id, ', ')
 
 416           FROM sepa_export_message_ids semi
 
 417           WHERE semi.sepa_export_id = se.id) AS message_ids,
 
 422            CASE WHEN COALESCE(emp.name, '') <> '' THEN emp.name ELSE emp.login END AS name
 
 424        ) AS e ON (se.employee_id = e.id)
 
 426        ORDER BY $sort_spec{sql}|;
 
 428   my $results = selectall_hashref_query($form, $dbh, $query, @values);
 
 430   $main::lxdebug->leave_sub();
 
 436   my ($self, %params) = @_;
 
 437   $main::lxdebug->enter_sub();
 
 439   my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, %params);
 
 441   $::lxdebug->leave_sub;
 
 449   Common::check_params(\%params, qw(items));
 
 451   my $myconfig = \%main::myconfig;
 
 452   my $form     = $main::form;
 
 453   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 454   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 455   my $mult     = $params{vc} eq 'customer' ? -1         : 1;
 
 458   my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 460   my @items    = ref $params{items} eq 'ARRAY' ? @{ $params{items} } : ($params{items});
 
 463     'get_item'       => [ qq|SELECT sei.*
 
 464                              FROM sepa_export_items sei
 
 467     'get_arap'       => [ qq|SELECT at.chart_id
 
 469                              LEFT JOIN chart c ON (at.chart_id = c.id)
 
 471                                AND ((c.link LIKE '%:${ARAP}') OR (c.link LIKE '${ARAP}:%') OR (c.link = '${ARAP}'))
 
 474     'add_acc_trans'  => [ qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate,       source, memo, taxkey, tax_id ,                                     chart_link)
 
 475                              VALUES                (?,        ?,        ?,      ?,         current_date, ?,      '',   0,      (SELECT id FROM tax WHERE taxkey=0 LIMIT 1), (SELECT link FROM chart WHERE id=?))| ],
 
 477     'update_arap'    => [ qq|UPDATE ${arap}
 
 481     'finish_item'    => [ qq|UPDATE sepa_export_items
 
 482                              SET execution_date = ?, executed = TRUE
 
 485     'has_unexecuted' => [ qq|SELECT sei1.id
 
 486                              FROM sepa_export_items sei1
 
 487                              WHERE (sei1.sepa_export_id = (SELECT sei2.sepa_export_id
 
 488                                                            FROM sepa_export_items sei2
 
 490                                AND NOT COALESCE(sei1.executed, FALSE)
 
 493     'do_close'       => [ qq|UPDATE sepa_export
 
 494                              SET executed = TRUE, closed = TRUE
 
 498   map { unshift @{ $_ }, prepare_query($form, $dbh, $_->[0]) } values %handles;
 
 500   foreach my $item (@items) {
 
 502     my $item_id = conv_i($item->{id});
 
 504     # Retrieve the item data belonging to the ID.
 
 505     do_statement($form, @{ $handles{get_item} }, $item_id);
 
 506     my $orig_item = $handles{get_item}->[0]->fetchrow_hashref();
 
 508     next if (!$orig_item);
 
 510     # fetch item_id via Rose (same id as orig_item)
 
 511     my $sepa_export_item = SL::DB::Manager::SepaExportItem->find_by( id => $item_id);
 
 515     if ( $sepa_export_item->ar_id ) {
 
 516       $invoice = SL::DB::Manager::Invoice->find_by( id => $sepa_export_item->ar_id);
 
 517     } elsif ( $sepa_export_item->ap_id ) {
 
 518       $invoice = SL::DB::Manager::PurchaseInvoice->find_by( id => $sepa_export_item->ap_id);
 
 520       die "sepa_export_item needs either ar_id or ap_id\n";
 
 523     $invoice->pay_invoice(amount       => $sepa_export_item->amount,
 
 524                           payment_type => $sepa_export_item->payment_type,
 
 525                           chart_id     => $sepa_export_item->chart_id,
 
 526                           source       => $sepa_export_item->reference,
 
 527                           transdate    => $item->{execution_date},  # value from user form
 
 530     # Update the item to reflect that it has been posted.
 
 531     do_statement($form, @{ $handles{finish_item} }, $item->{execution_date}, $item_id);
 
 533     # Check whether or not we can close the export itself if there are no unexecuted items left.
 
 534     do_statement($form, @{ $handles{has_unexecuted} }, $item_id);
 
 535     my ($has_unexecuted) = $handles{has_unexecuted}->[0]->fetchrow_array();
 
 537     if (!$has_unexecuted) {
 
 538       do_statement($form, @{ $handles{do_close} }, $orig_item->{sepa_export_id});
 
 542   map { $_->[0]->finish() } values %handles;
 
 554 SL::SEPA - Base class for SEPA objects
 
 558  # get all open invoices we like to pay via SEPA
 
 559  my $invoices = SL::SEPA->retrieve_open_invoices(vc => 'vendor');
 
 561  # add some IBAN and purposes for open transaction
 
 562  # and assign this to a SEPA export
 
 563  my $id = SL::SEPA->create_export('employee'       => $::myconfig{login},
 
 564                                  'bank_transfers' => \@bank_transfers,
 
 569 This is the base class for SEPA. SEPA and the underlying directories
 
 570 (SEPA::XML etc) are used to genereate valid XML files for the SEPA
 
 571 (Single European Payment Area) specification and offers this structure
 
 572 as a download via a xml file.
 
 574 An export can have one or more transaction which have to
 
 575 comply to the specification (IBAN, BIC, amount, purpose, etc).
 
 577 Furthermore kivitendo sepa exports have two
 
 578 valid states: Open or closed and executed or not executed.
 
 580 The state closed can be set via a user interface and the
 
 581 state executed is automatically assigned if the action payment
 
 586 =head2 C<undo_export> $sepa_export_id
 
 588 Needs a valid sepa_export id and deletes the sepa export if
 
 589 the state of the export is neither executed nor closed.
 
 590 Returns undef if the deletion was successfully.
 
 591 Otherwise the function just dies with a short notice of the id.