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   # open_amount is not the current open amount according to bookkeeping, but
 
  33   # the open amount minus the SEPA transfer amounts that haven't been closed yet
 
  36        SELECT ${arap}.id, ${arap}.invnumber, ${arap}.transdate, ${arap}.${vc}_id as vc_id, ${arap}.amount AS invoice_amount, ${arap}.invoice,
 
  37          (${arap}.transdate + pt.terms_skonto) as skonto_date, (pt.percent_skonto * 100) as percent_skonto,
 
  38          (${arap}.amount - (${arap}.amount * pt.percent_skonto)) as amount_less_skonto,
 
  39          (${arap}.amount * pt.percent_skonto) as skonto_amount,
 
  40          vc.name AS vcname, vc.language_id, ${arap}.duedate as duedate, ${arap}.direct_debit,
 
  41          vc.${vc_vc_id} as vc_vc_id,
 
  43          COALESCE(vc.iban, '') <> '' AND COALESCE(vc.bic, '') <> '' ${mandate} AS vc_bank_info_ok,
 
  45          ${arap}.amount - ${arap}.paid - COALESCE(open_transfers.amount, 0) AS open_amount,
 
  46          COALESCE(open_transfers.amount, 0) AS transfer_amount,
 
  47          pt.description as pt_description
 
  50        LEFT JOIN ${vc} vc ON (${arap}.${vc}_id = vc.id)
 
  51        LEFT JOIN (SELECT sei.${arap}_id, SUM(sei.amount) + SUM(COALESCE(sei.skonto_amount,0)) AS amount
 
  52                   FROM sepa_export_items sei
 
  53                   LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id)
 
  56                   GROUP BY sei.${arap}_id)
 
  57          AS open_transfers ON (${arap}.id = open_transfers.${arap}_id)
 
  59        LEFT JOIN payment_terms pt ON (${arap}.payment_id = pt.id)
 
  61        WHERE ${arap}.amount > (COALESCE(open_transfers.amount, 0) + ${arap}.paid)
 
  63        ORDER BY lower(vc.name) ASC, lower(${arap}.invnumber) ASC
 
  65     #  $main::lxdebug->message(LXDebug->DEBUG2(),"sepa add query:".$query);
 
  67   my $results = selectall_hashref_query($form, $dbh, $query);
 
  69   # add some more data to $results:
 
  70   # create drop-down data for payment types and suggest amount to be paid according
 
  71   # to open amount or skonto
 
  73   foreach my $result ( @$results ) {
 
  74     my $invoice = $vc eq 'customer' ? SL::DB::Manager::Invoice->find_by(         id => $result->{id} )
 
  75                                     : SL::DB::Manager::PurchaseInvoice->find_by( id => $result->{id} );
 
  77     $invoice->get_payment_suggestions(sepa => 1); # consider amounts of open entries in sepa_export_items
 
  78     $result->{skonto_amount}             = $invoice->skonto_amount;
 
  79     $result->{within_skonto_period}      = $invoice->within_skonto_period;
 
  80     $result->{invoice_amount_suggestion} = $invoice->{invoice_amount_suggestion};
 
  81     $result->{payment_select_options}    = $invoice->{payment_select_options};
 
  84   $main::lxdebug->leave_sub();
 
  90   my ($self, %params) = @_;
 
  91   $main::lxdebug->enter_sub();
 
  93   my $rc = SL::DB->client->with_transaction(\&_create_export, $self, %params);
 
  95   $::lxdebug->leave_sub;
 
 103   Common::check_params(\%params, qw(employee bank_transfers vc));
 
 105   my $myconfig = \%main::myconfig;
 
 106   my $form     = $main::form;
 
 107   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 108   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 111   my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 113   my ($export_id) = selectfirst_array_query($form, $dbh, qq|SELECT nextval('sepa_export_id_seq')|);
 
 115     qq|INSERT INTO sepa_export (id, employee_id, vc)
 
 116        VALUES (?, (SELECT id
 
 118                    WHERE login = ?), ?)|;
 
 119   do_query($form, $dbh, $query, $export_id, $params{employee}, $vc);
 
 121   my $q_item_id = qq|SELECT nextval('id')|;
 
 122   my $h_item_id = prepare_query($form, $dbh, $q_item_id);
 
 123   my $c_mandate = $params{vc} eq 'customer' ? ', vc_mandator_id, vc_mandate_date_of_signature' : '';
 
 124   my $p_mandate = $params{vc} eq 'customer' ? ', ?, ?' : '';
 
 127     qq|INSERT INTO sepa_export_items (id,          sepa_export_id,           ${arap}_id,  chart_id,
 
 128                                       amount,      requested_execution_date, reference,   end_to_end_id,
 
 129                                       our_iban,    our_bic,                  vc_iban,     vc_bic,
 
 130                                       skonto_amount, payment_type ${c_mandate})
 
 135   my $h_insert = prepare_query($form, $dbh, $q_insert);
 
 138     qq|SELECT arap.invnumber,
 
 141           LEFT JOIN chart c ON (at.chart_id = c.id)
 
 142           WHERE (at.trans_id = ?)
 
 143             AND (c.link LIKE '%${ARAP}_paid%'))
 
 146           FROM sepa_export_items sei
 
 147           WHERE (sei.ap_id = ?))
 
 151   my $h_reference = prepare_query($form, $dbh, $q_reference);
 
 155   foreach my $transfer (@{ $params{bank_transfers} }) {
 
 156     if (!$transfer->{reference}) {
 
 157       do_statement($form, $h_reference, $q_reference, (conv_i($transfer->{"${arap}_id"})) x 3);
 
 159       my ($invnumber, $num_payments) = $h_reference->fetchrow_array();
 
 162       $transfer->{reference} = "${invnumber}-${num_payments}";
 
 165     $h_item_id->execute() || $::form->dberror($q_item_id);
 
 166     my ($item_id)      = $h_item_id->fetchrow_array();
 
 168     my $end_to_end_id  = strftime "LXO%Y%m%d%H%M%S", localtime;
 
 169     my $item_id_len    = length "$item_id";
 
 170     my $num_zeroes     = 35 - $item_id_len - length $end_to_end_id;
 
 171     $end_to_end_id    .= '0' x $num_zeroes if (0 < $num_zeroes);
 
 172     $end_to_end_id    .= $item_id;
 
 173     $end_to_end_id     = substr $end_to_end_id, 0, 35;
 
 175     my @values = ($item_id,                          $export_id,
 
 176                   conv_i($transfer->{"${arap}_id"}), conv_i($transfer->{chart_id}),
 
 177                   $transfer->{amount},               conv_date($transfer->{requested_execution_date}),
 
 178                   $transfer->{reference},            $end_to_end_id,
 
 179                   map { my $pfx = $_; map { $transfer->{"${pfx}_${_}"} } qw(iban bic) } qw(our vc));
 
 180     # save value of skonto_amount and payment_type
 
 181     if ( $transfer->{payment_type} eq 'without_skonto' ) {
 
 183     } elsif ($transfer->{payment_type} eq 'difference_as_skonto' ) {
 
 184       push(@values, $transfer->{amount});
 
 185     } elsif ($transfer->{payment_type} eq 'with_skonto_pt' ) {
 
 186       push(@values, $transfer->{skonto_amount});
 
 188       die "illegal payment_type: " . $transfer->{payment_type} . "\n";
 
 190     push(@values, $transfer->{payment_type});
 
 192     push @values, $transfer->{vc_mandator_id}, conv_date($transfer->{vc_mandate_date_of_signature}) if $params{vc} eq 'customer';
 
 194     do_statement($form, $h_insert, $q_insert, @values);
 
 198   $h_item_id->finish();
 
 203 sub retrieve_export {
 
 204   $main::lxdebug->enter_sub();
 
 209   Common::check_params(\%params, qw(id vc));
 
 211   my $myconfig = \%main::myconfig;
 
 212   my $form     = $main::form;
 
 213   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 214   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 216   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
 218   my ($joins, $columns);
 
 220   if ($params{details}) {
 
 221     $columns = ', arap.invoice';
 
 222     $joins   = "LEFT JOIN ${arap} arap ON (se.${arap}_id = arap.id)";
 
 227          CASE WHEN COALESCE(e.name, '') <> '' THEN e.name ELSE e.login END AS employee
 
 229        LEFT JOIN employee e ON (se.employee_id = e.id)
 
 232   my $export = selectfirst_hashref_query($form, $dbh, $query, conv_i($params{id}));
 
 235     my ($columns, $joins);
 
 237     my $mandator_id = $params{vc} eq 'customer' ? ', mandator_id, mandate_date_of_signature' : '';
 
 239     if ($params{details}) {
 
 240       $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}|;
 
 241       $joins   = qq|LEFT JOIN ${arap} arap ON (sei.${arap}_id = arap.id)
 
 242                     LEFT JOIN ${vc} vc     ON (arap.${vc}_id  = vc.id)
 
 243                     LEFT JOIN chart c      ON (sei.chart_id   = c.id)|;
 
 246     $query = qq|SELECT sei.*
 
 248                 FROM sepa_export_items sei
 
 250                 WHERE sei.sepa_export_id = ?|;
 
 252     $export->{items} = selectall_hashref_query($form, $dbh, $query, conv_i($params{id}));
 
 255     $export->{items} = [];
 
 258   $main::lxdebug->leave_sub();
 
 264   $main::lxdebug->enter_sub();
 
 269   Common::check_params(\%params, qw(id));
 
 271   my $myconfig = \%main::myconfig;
 
 272   my $form     = $main::form;
 
 274   SL::DB->client->with_transaction(sub {
 
 275     my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 277     my @ids          = ref $params{id} eq 'ARRAY' ? @{ $params{id} } : ($params{id});
 
 278     my $placeholders = join ', ', ('?') x scalar @ids;
 
 279     my $query        = qq|UPDATE sepa_export SET closed = TRUE WHERE id IN ($placeholders)|;
 
 281     do_query($form, $dbh, $query, map { conv_i($_) } @ids);
 
 283   }) or do { die SL::DB->client->error };
 
 285   $main::lxdebug->leave_sub();
 
 289   $main::lxdebug->enter_sub();
 
 294   Common::check_params(\%params, qw(id));
 
 296   my $sepa_export = SL::DB::Manager::SepaExport->find_by(id => $params{id});
 
 298   croak "Not a valid SEPA Export id: $params{id}" unless $sepa_export;
 
 299   croak "Cannot undo closed exports."             if $sepa_export->closed;
 
 300   croak "Cannot undo executed exports."           if $sepa_export->executed;
 
 302   die "Could not undo $sepa_export->id" if !$sepa_export->delete();
 
 304   $main::lxdebug->leave_sub();
 
 308   $main::lxdebug->enter_sub();
 
 313   my $myconfig = \%main::myconfig;
 
 314   my $form     = $main::form;
 
 315   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 316   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 318   my $dbh      = $params{dbh} || $form->get_standard_dbh($myconfig);
 
 321     'id'          => [ 'se.id',                ],
 
 322     'export_date' => [ 'se.itime',             ],
 
 323     'employee'    => [ 'e.name',      'se.id', ],
 
 324     'executed'    => [ 'se.executed', 'se.id', ],
 
 325     'closed'      => [ 'se.closed',   'se.id', ],
 
 328   my %sort_spec = create_sort_spec('defs' => \%sort_columns, 'default' => 'id', 'column' => $params{sortorder}, 'dir' => $params{sortdir});
 
 330   my (@where, @values, @where_sub, @values_sub, %joins_sub);
 
 332   my $filter = $params{filter} || { };
 
 334   foreach (qw(executed closed)) {
 
 335     push @where, $filter->{$_} ? "se.$_" : "NOT se.$_" if (exists $filter->{$_});
 
 338   my %operators = ('from' => '>=',
 
 341   foreach my $dir (qw(from to)) {
 
 342     next unless ($filter->{"export_date_${dir}"});
 
 343     push @where,  "se.itime $operators{$dir} ?::date";
 
 344     push @values, $filter->{"export_date_${dir}"};
 
 347   if ($filter->{invnumber}) {
 
 348     push @where_sub,  "arap.invnumber ILIKE ?";
 
 349     push @values_sub, like($filter->{invnumber});
 
 350     $joins_sub{$arap} = 1;
 
 353   if ($filter->{message_id}) {
 
 354     push @values, like($filter->{message_id});
 
 357         SELECT sepa_export_id
 
 358         FROM sepa_export_message_ids
 
 359         WHERE message_id ILIKE ?
 
 365     push @where_sub,  "vc.name ILIKE ?";
 
 366     push @values_sub, like($filter->{vc});
 
 367     $joins_sub{$arap} = 1;
 
 371   foreach my $type (qw(requested_execution execution)) {
 
 372     foreach my $dir (qw(from to)) {
 
 373       next unless ($filter->{"${type}_date_${dir}"});
 
 374       push @where_sub,  "(items.${type}_date IS NOT NULL) AND (items.${type}_date $operators{$dir} ?)";
 
 375       push @values_sub, $filter->{"${type}_date_${_}"};
 
 381     $joins_sub    .= " LEFT JOIN ${arap} arap ON (items.${arap}_id = arap.id)" if ($joins_sub{$arap});
 
 382     $joins_sub    .= " LEFT JOIN ${vc} vc      ON (arap.${vc}_id   = vc.id)"   if ($joins_sub{vc});
 
 384     my $where_sub  = join(' AND ', map { "(${_})" } @where_sub);
 
 386     my $query_sub  = qq|se.id IN (SELECT items.sepa_export_id
 
 387                                   FROM sepa_export_items items
 
 391     push @where,  $query_sub;
 
 392     push @values, @values_sub;
 
 395   push @where,  'se.vc = ?';
 
 398   my $where = @where ? ' WHERE ' . join(' AND ', map { "(${_})" } @where) : '';
 
 401     qq|SELECT se.id, se.employee_id, se.executed, se.closed, itime::date AS export_date,
 
 403           FROM sepa_export_items sei
 
 404           WHERE (sei.sepa_export_id = se.id)) AS num_invoices,
 
 405          (SELECT SUM(sei.amount)
 
 406           FROM sepa_export_items sei
 
 407           WHERE (sei.sepa_export_id = se.id)) AS sum_amounts,
 
 408          (SELECT string_agg(semi.message_id, ', ')
 
 409           FROM sepa_export_message_ids semi
 
 410           WHERE semi.sepa_export_id = se.id) AS message_ids,
 
 415            CASE WHEN COALESCE(emp.name, '') <> '' THEN emp.name ELSE emp.login END AS name
 
 417        ) AS e ON (se.employee_id = e.id)
 
 419        ORDER BY $sort_spec{sql}|;
 
 421   my $results = selectall_hashref_query($form, $dbh, $query, @values);
 
 423   $main::lxdebug->leave_sub();
 
 429   my ($self, %params) = @_;
 
 430   $main::lxdebug->enter_sub();
 
 432   my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, %params);
 
 434   $::lxdebug->leave_sub;
 
 442   Common::check_params(\%params, qw(items));
 
 444   my $myconfig = \%main::myconfig;
 
 445   my $form     = $main::form;
 
 446   my $vc       = $params{vc} eq 'customer' ? 'customer' : 'vendor';
 
 447   my $arap     = $params{vc} eq 'customer' ? 'ar'       : 'ap';
 
 448   my $mult     = $params{vc} eq 'customer' ? -1         : 1;
 
 451   my $dbh      = $params{dbh} || SL::DB->client->dbh;
 
 453   my @items    = ref $params{items} eq 'ARRAY' ? @{ $params{items} } : ($params{items});
 
 456     'get_item'       => [ qq|SELECT sei.*
 
 457                              FROM sepa_export_items sei
 
 460     'get_arap'       => [ qq|SELECT at.chart_id
 
 462                              LEFT JOIN chart c ON (at.chart_id = c.id)
 
 464                                AND ((c.link LIKE '%:${ARAP}') OR (c.link LIKE '${ARAP}:%') OR (c.link = '${ARAP}'))
 
 467     'add_acc_trans'  => [ qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate,       source, memo, taxkey, tax_id ,                                     chart_link)
 
 468                              VALUES                (?,        ?,        ?,      ?,         current_date, ?,      '',   0,      (SELECT id FROM tax WHERE taxkey=0 LIMIT 1), (SELECT link FROM chart WHERE id=?))| ],
 
 470     'update_arap'    => [ qq|UPDATE ${arap}
 
 474     'finish_item'    => [ qq|UPDATE sepa_export_items
 
 475                              SET execution_date = ?, executed = TRUE
 
 478     'has_unexecuted' => [ qq|SELECT sei1.id
 
 479                              FROM sepa_export_items sei1
 
 480                              WHERE (sei1.sepa_export_id = (SELECT sei2.sepa_export_id
 
 481                                                            FROM sepa_export_items sei2
 
 483                                AND NOT COALESCE(sei1.executed, FALSE)
 
 486     'do_close'       => [ qq|UPDATE sepa_export
 
 487                              SET executed = TRUE, closed = TRUE
 
 491   map { unshift @{ $_ }, prepare_query($form, $dbh, $_->[0]) } values %handles;
 
 493   foreach my $item (@items) {
 
 495     my $item_id = conv_i($item->{id});
 
 497     # Retrieve the item data belonging to the ID.
 
 498     do_statement($form, @{ $handles{get_item} }, $item_id);
 
 499     my $orig_item = $handles{get_item}->[0]->fetchrow_hashref();
 
 501     next if (!$orig_item);
 
 503     # fetch item_id via Rose (same id as orig_item)
 
 504     my $sepa_export_item = SL::DB::Manager::SepaExportItem->find_by( id => $item_id);
 
 508     if ( $sepa_export_item->ar_id ) {
 
 509       $invoice = SL::DB::Manager::Invoice->find_by( id => $sepa_export_item->ar_id);
 
 510     } elsif ( $sepa_export_item->ap_id ) {
 
 511       $invoice = SL::DB::Manager::PurchaseInvoice->find_by( id => $sepa_export_item->ap_id);
 
 513       die "sepa_export_item needs either ar_id or ap_id\n";
 
 516     $invoice->pay_invoice(amount       => $sepa_export_item->amount,
 
 517                           payment_type => $sepa_export_item->payment_type,
 
 518                           chart_id     => $sepa_export_item->chart_id,
 
 519                           source       => $sepa_export_item->reference,
 
 520                           transdate    => $item->{execution_date},  # value from user form
 
 523     # Update the item to reflect that it has been posted.
 
 524     do_statement($form, @{ $handles{finish_item} }, $item->{execution_date}, $item_id);
 
 526     # Check whether or not we can close the export itself if there are no unexecuted items left.
 
 527     do_statement($form, @{ $handles{has_unexecuted} }, $item_id);
 
 528     my ($has_unexecuted) = $handles{has_unexecuted}->[0]->fetchrow_array();
 
 530     if (!$has_unexecuted) {
 
 531       do_statement($form, @{ $handles{do_close} }, $orig_item->{sepa_export_id});
 
 535   map { $_->[0]->finish() } values %handles;
 
 547 SL::SEPA - Base class for SEPA objects
 
 551  # get all open invoices we like to pay via SEPA
 
 552  my $invoices = SL::SEPA->retrieve_open_invoices(vc => 'vendor');
 
 554  # add some IBAN and purposes for open transaction
 
 555  # and assign this to a SEPA export
 
 556  my $id = SL::SEPA->create_export('employee'       => $::myconfig{login},
 
 557                                  'bank_transfers' => \@bank_transfers,
 
 562 This is the base class for SEPA. SEPA and the underlying directories
 
 563 (SEPA::XML etc) are used to genereate valid XML files for the SEPA
 
 564 (Single European Payment Area) specification and offers this structure
 
 565 as a download via a xml file.
 
 567 An export can have one or more transaction which have to
 
 568 comply to the specification (IBAN, BIC, amount, purpose, etc).
 
 570 Furthermore kivitendo sepa exports have two
 
 571 valid states: Open or closed and executed or not executed.
 
 573 The state closed can be set via a user interface and the
 
 574 state executed is automatically assigned if the action payment
 
 579 =head2 C<undo_export> $sepa_export_id
 
 581 Needs a valid sepa_export id and deletes the sepa export if
 
 582 the state of the export is neither executed nor closed.
 
 583 Returns undef if the deletion was successfully.
 
 584 Otherwise the function just dies with a short notice of the id.