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.