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 my $is_sepa_blocked = $params{vc} eq 'customer' ? 'FALSE' : "${arap}.is_sepa_blocked";
34 # open_amount is not the current open amount according to bookkeeping, but
35 # the open amount minus the SEPA transfer amounts that haven't been closed yet
38 SELECT ${arap}.id, ${arap}.invnumber, ${arap}.transdate, ${arap}.${vc}_id as vc_id, ${arap}.amount AS invoice_amount, ${arap}.invoice,
39 (${arap}.transdate + pt.terms_skonto) as skonto_date, (pt.percent_skonto * 100) as percent_skonto,
40 (${arap}.amount - (${arap}.amount * pt.percent_skonto)) as amount_less_skonto,
41 (${arap}.amount * pt.percent_skonto) as skonto_amount,
42 vc.name AS vcname, vc.language_id, ${arap}.duedate as duedate, ${arap}.direct_debit,
43 ${is_sepa_blocked} AS is_sepa_blocked,
44 vc.${vc_vc_id} as vc_vc_id,
46 COALESCE(vc.iban, '') <> '' AND COALESCE(vc.bic, '') <> '' ${mandate} AS vc_bank_info_ok,
48 ${arap}.amount - ${arap}.paid - COALESCE(open_transfers.amount, 0) AS open_amount,
49 COALESCE(open_transfers.amount, 0) AS transfer_amount,
50 pt.description as pt_description,
51 (current_date < (${arap}.transdate + pt.terms_skonto)) as within_skonto_period
53 LEFT JOIN ${vc} vc ON (${arap}.${vc}_id = vc.id)
54 LEFT JOIN (SELECT sei.${arap}_id, SUM(sei.amount) + SUM(COALESCE(sei.skonto_amount,0)) AS amount
55 FROM sepa_export_items sei
56 LEFT JOIN sepa_export se ON (sei.sepa_export_id = se.id)
59 GROUP BY sei.${arap}_id)
60 AS open_transfers ON (${arap}.id = open_transfers.${arap}_id)
62 LEFT JOIN payment_terms pt ON (${arap}.payment_id = pt.id)
64 WHERE (${arap}.amount - (COALESCE(open_transfers.amount, 0) + ${arap}.paid)) >= 0.01
66 ORDER BY lower(vc.name) ASC, lower(${arap}.invnumber) ASC
68 # $main::lxdebug->message(LXDebug->DEBUG2(),"sepa add query:".$query);
70 my $results = selectall_hashref_query($form, $dbh, $query);
72 # add some more data to $results:
73 # create drop-down data for payment types and suggest amount to be paid according
74 # to open amount or skonto
75 # One minor fault: amount_less_skonto does not subtract the not yet booked sepa transfer amounts
77 foreach my $result ( @$results ) {
79 push @options, { payment_type => 'without_skonto', display => t8('without skonto') };
80 push @options, { payment_type => 'with_skonto_pt', display => t8('with skonto acc. to pt'), selected => 1 } if $result->{within_skonto_period};
81 $result->{payment_select_options} = \@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 "KIVITENDO%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 = ?
253 $export->{items} = selectall_hashref_query($form, $dbh, $query, conv_i($params{id}));
256 $export->{items} = [];
259 $main::lxdebug->leave_sub();
265 $main::lxdebug->enter_sub();
270 Common::check_params(\%params, qw(id));
272 my $myconfig = \%main::myconfig;
273 my $form = $main::form;
275 SL::DB->client->with_transaction(sub {
276 my $dbh = $params{dbh} || SL::DB->client->dbh;
278 my @ids = ref $params{id} eq 'ARRAY' ? @{ $params{id} } : ($params{id});
279 my $placeholders = join ', ', ('?') x scalar @ids;
280 my $query = qq|UPDATE sepa_export SET closed = TRUE WHERE id IN ($placeholders)|;
282 do_query($form, $dbh, $query, map { conv_i($_) } @ids);
284 }) or do { die SL::DB->client->error };
286 $main::lxdebug->leave_sub();
290 $main::lxdebug->enter_sub();
295 Common::check_params(\%params, qw(id));
297 my $sepa_export = SL::DB::Manager::SepaExport->find_by(id => $params{id});
299 croak "Not a valid SEPA Export id: $params{id}" unless $sepa_export;
300 croak "Cannot undo closed exports." if $sepa_export->closed;
301 croak "Cannot undo executed exports." if $sepa_export->executed;
303 die "Could not undo $sepa_export->id" if !$sepa_export->delete();
305 $main::lxdebug->leave_sub();
309 $main::lxdebug->enter_sub();
314 my $myconfig = \%main::myconfig;
315 my $form = $main::form;
316 my $vc = $params{vc} eq 'customer' ? 'customer' : 'vendor';
317 my $arap = $params{vc} eq 'customer' ? 'ar' : 'ap';
319 my $dbh = $params{dbh} || $form->get_standard_dbh($myconfig);
322 'id' => [ 'se.id', ],
323 'export_date' => [ 'se.itime', ],
324 'employee' => [ 'e.name', 'se.id', ],
325 'executed' => [ 'se.executed', 'se.id', ],
326 'closed' => [ 'se.closed', 'se.id', ],
329 my %sort_spec = create_sort_spec('defs' => \%sort_columns, 'default' => 'id', 'column' => $params{sortorder}, 'dir' => $params{sortdir});
331 my (@where, @values, @where_sub, @values_sub, %joins_sub);
333 my $filter = $params{filter} || { };
335 foreach (qw(executed closed)) {
336 push @where, $filter->{$_} ? "se.$_" : "NOT se.$_" if (exists $filter->{$_});
339 my %operators = ('from' => '>=',
342 foreach my $dir (qw(from to)) {
343 next unless ($filter->{"export_date_${dir}"});
344 push @where, "se.itime $operators{$dir} ?::date";
345 push @values, $filter->{"export_date_${dir}"};
348 if ($filter->{invnumber}) {
349 push @where_sub, "arap.invnumber ILIKE ?";
350 push @values_sub, like($filter->{invnumber});
351 $joins_sub{$arap} = 1;
354 if ($filter->{message_id}) {
355 push @values, like($filter->{message_id});
358 SELECT sepa_export_id
359 FROM sepa_export_message_ids
360 WHERE message_id ILIKE ?
366 push @where_sub, "vc.name ILIKE ?";
367 push @values_sub, like($filter->{vc});
368 $joins_sub{$arap} = 1;
372 foreach my $type (qw(requested_execution execution)) {
373 foreach my $dir (qw(from to)) {
374 next unless ($filter->{"${type}_date_${dir}"});
375 push @where_sub, "(items.${type}_date IS NOT NULL) AND (items.${type}_date $operators{$dir} ?)";
376 push @values_sub, $filter->{"${type}_date_${_}"};
382 $joins_sub .= " LEFT JOIN ${arap} arap ON (items.${arap}_id = arap.id)" if ($joins_sub{$arap});
383 $joins_sub .= " LEFT JOIN ${vc} vc ON (arap.${vc}_id = vc.id)" if ($joins_sub{vc});
385 my $where_sub = join(' AND ', map { "(${_})" } @where_sub);
387 my $query_sub = qq|se.id IN (SELECT items.sepa_export_id
388 FROM sepa_export_items items
392 push @where, $query_sub;
393 push @values, @values_sub;
396 push @where, 'se.vc = ?';
399 my $where = @where ? ' WHERE ' . join(' AND ', map { "(${_})" } @where) : '';
402 qq|SELECT se.id, se.employee_id, se.executed, se.closed, itime::date AS export_date,
404 FROM sepa_export_items sei
405 WHERE (sei.sepa_export_id = se.id)) AS num_invoices,
406 (SELECT SUM(sei.amount)
407 FROM sepa_export_items sei
408 WHERE (sei.sepa_export_id = se.id)) AS sum_amounts,
409 (SELECT string_agg(semi.message_id, ', ')
410 FROM sepa_export_message_ids semi
411 WHERE semi.sepa_export_id = se.id) AS message_ids,
416 CASE WHEN COALESCE(emp.name, '') <> '' THEN emp.name ELSE emp.login END AS name
418 ) AS e ON (se.employee_id = e.id)
420 ORDER BY $sort_spec{sql}|;
422 my $results = selectall_hashref_query($form, $dbh, $query, @values);
424 $main::lxdebug->leave_sub();
430 my ($self, %params) = @_;
431 $main::lxdebug->enter_sub();
433 my $rc = SL::DB->client->with_transaction(\&_post_payment, $self, %params);
435 $::lxdebug->leave_sub;
443 Common::check_params(\%params, qw(items));
445 my $myconfig = \%main::myconfig;
446 my $form = $main::form;
447 my $vc = $params{vc} eq 'customer' ? 'customer' : 'vendor';
448 my $arap = $params{vc} eq 'customer' ? 'ar' : 'ap';
449 my $mult = $params{vc} eq 'customer' ? -1 : 1;
452 my $dbh = $params{dbh} || SL::DB->client->dbh;
454 my @items = ref $params{items} eq 'ARRAY' ? @{ $params{items} } : ($params{items});
457 'get_item' => [ qq|SELECT sei.*
458 FROM sepa_export_items sei
461 'get_arap' => [ qq|SELECT at.chart_id
463 LEFT JOIN chart c ON (at.chart_id = c.id)
465 AND ((c.link LIKE '%:${ARAP}') OR (c.link LIKE '${ARAP}:%') OR (c.link = '${ARAP}'))
468 'add_acc_trans' => [ qq|INSERT INTO acc_trans (trans_id, chart_id, amount, transdate, gldate, source, memo, taxkey, tax_id , chart_link)
469 VALUES (?, ?, ?, ?, current_date, ?, '', 0, (SELECT id FROM tax WHERE taxkey=0 LIMIT 1), (SELECT link FROM chart WHERE id=?))| ],
471 'update_arap' => [ qq|UPDATE ${arap}
475 'finish_item' => [ qq|UPDATE sepa_export_items
476 SET execution_date = ?, executed = TRUE
479 'has_unexecuted' => [ qq|SELECT sei1.id
480 FROM sepa_export_items sei1
481 WHERE (sei1.sepa_export_id = (SELECT sei2.sepa_export_id
482 FROM sepa_export_items sei2
484 AND NOT COALESCE(sei1.executed, FALSE)
487 'do_close' => [ qq|UPDATE sepa_export
488 SET executed = TRUE, closed = TRUE
492 map { unshift @{ $_ }, prepare_query($form, $dbh, $_->[0]) } values %handles;
494 foreach my $item (@items) {
496 my $item_id = conv_i($item->{id});
498 # Retrieve the item data belonging to the ID.
499 do_statement($form, @{ $handles{get_item} }, $item_id);
500 my $orig_item = $handles{get_item}->[0]->fetchrow_hashref();
502 next if (!$orig_item);
504 # fetch item_id via Rose (same id as orig_item)
505 my $sepa_export_item = SL::DB::Manager::SepaExportItem->find_by( id => $item_id);
509 if ( $sepa_export_item->ar_id ) {
510 $invoice = SL::DB::Manager::Invoice->find_by( id => $sepa_export_item->ar_id);
511 } elsif ( $sepa_export_item->ap_id ) {
512 $invoice = SL::DB::Manager::PurchaseInvoice->find_by( id => $sepa_export_item->ap_id);
514 die "sepa_export_item needs either ar_id or ap_id\n";
517 $invoice->pay_invoice(amount => $sepa_export_item->amount,
518 payment_type => $sepa_export_item->payment_type,
519 chart_id => $sepa_export_item->chart_id,
520 source => $sepa_export_item->reference,
521 transdate => $item->{execution_date}, # value from user form
524 # Update the item to reflect that it has been posted.
525 do_statement($form, @{ $handles{finish_item} }, $item->{execution_date}, $item_id);
527 # Check whether or not we can close the export itself if there are no unexecuted items left.
528 do_statement($form, @{ $handles{has_unexecuted} }, $item_id);
529 my ($has_unexecuted) = $handles{has_unexecuted}->[0]->fetchrow_array();
531 if (!$has_unexecuted) {
532 do_statement($form, @{ $handles{do_close} }, $orig_item->{sepa_export_id});
536 map { $_->[0]->finish() } values %handles;
548 SL::SEPA - Base class for SEPA objects
552 # get all open invoices we like to pay via SEPA
553 my $invoices = SL::SEPA->retrieve_open_invoices(vc => 'vendor');
555 # add some IBAN and purposes for open transaction
556 # and assign this to a SEPA export
557 my $id = SL::SEPA->create_export('employee' => $::myconfig{login},
558 'bank_transfers' => \@bank_transfers,
563 This is the base class for SEPA. SEPA and the underlying directories
564 (SEPA::XML etc) are used to genereate valid XML files for the SEPA
565 (Single European Payment Area) specification and offers this structure
566 as a download via a xml file.
568 An export can have one or more transaction which have to
569 comply to the specification (IBAN, BIC, amount, purpose, etc).
571 Furthermore kivitendo sepa exports have two
572 valid states: Open or closed and executed or not executed.
574 The state closed can be set via a user interface and the
575 state executed is automatically assigned if the action payment
580 =head2 C<undo_export> $sepa_export_id
582 Needs a valid sepa_export id and deletes the sepa export if
583 the state of the export is neither executed nor closed.
584 Returns undef if the deletion was successfully.
585 Otherwise the function just dies with a short notice of the id.