Revision d62a113d
Von Moritz Bunkus vor 7 Monaten hinzugefügt
SL/SEPA/XML.pm | ||
---|---|---|
5 | 5 |
|
6 | 6 |
use Carp; |
7 | 7 |
use Encode; |
8 |
use List::Util qw(first sum); |
|
9 | 8 |
use List::MoreUtils qw(any); |
9 |
use List::Util qw(first sum); |
|
10 | 10 |
use POSIX qw(strftime); |
11 | 11 |
use XML::Writer; |
12 | 12 |
|
13 | 13 |
use SL::Iconv; |
14 | 14 |
use SL::SEPA::XML::Transaction; |
15 | 15 |
|
16 |
use constant V3_0_0 => 3_000_000; |
|
17 |
use constant V3_8_0 => 3_800_000; |
|
18 |
|
|
19 |
sub get_supported_versions { |
|
20 |
return ( |
|
21 |
versions => [ |
|
22 |
{ id => V3_0_0(), description => "v3.0.0" }, |
|
23 |
{ id => V3_8_0(), description => "v3.8.0" }, |
|
24 |
], |
|
25 |
default => get_default_version(), |
|
26 |
); |
|
27 |
} |
|
28 |
|
|
29 |
sub get_default_version { |
|
30 |
my $today = DateTime->today_local; |
|
31 |
my $cutoff_v3_8_0 = DateTime->new_local(year => 2025, month => 10, day => 1); |
|
32 |
|
|
33 |
return V3_8_0() if $today >= $cutoff_v3_8_0; |
|
34 |
return V3_0_0(); |
|
35 |
} |
|
36 |
|
|
37 |
sub is_version_valid { |
|
38 |
shift while @_ && ref($_[0]); |
|
39 |
|
|
40 |
my $id = $_[0]; |
|
41 |
my %versions = get_supported_versions(); |
|
42 |
|
|
43 |
return any { $_->{id} == $id } @{ $versions{versions} }; |
|
44 |
} |
|
45 |
|
|
16 | 46 |
sub new { |
17 | 47 |
my $class = shift; |
18 | 48 |
my $self = {}; |
... | ... | |
31 | 61 |
$self->{transactions} = []; |
32 | 62 |
$self->{src_charset} = 'UTF-8'; |
33 | 63 |
$self->{grouped} = 0; |
64 |
$self->{version} = get_default_version(); |
|
34 | 65 |
|
35 |
map { $self->{$_} = $params{$_} if (exists $params{$_}) } qw(src_charset company creditor_id message_id grouped collection); |
|
66 |
map { $self->{$_} = $params{$_} if (exists $params{$_}) } qw(src_charset company creditor_id message_id grouped collection version);
|
|
36 | 67 |
|
37 | 68 |
$self->{iconv} = SL::Iconv->new($self->{src_charset}, "UTF-8") || croak "Unsupported source charset $self->{src_charset}."; |
38 | 69 |
|
39 | 70 |
my $missing_parameter = first { !$self->{$_} } qw(company message_id); |
40 | 71 |
croak "Missing parameter: $missing_parameter" if ($missing_parameter); |
41 | 72 |
croak "Missing parameter: creditor_id" if !$self->{creditor_id} && $self->{collection}; |
73 |
croak "Invalid parameter: version" if !is_version_valid($self->{version}); |
|
42 | 74 |
|
43 | 75 |
map { $self->{$_} = $self->_replace_special_chars($self->{iconv}->convert($self->{$_})) } qw(company message_id creditor_id); |
44 | 76 |
} |
... | ... | |
124 | 156 |
return substr $string, 0, 35; |
125 | 157 |
} |
126 | 158 |
|
159 |
sub _emit_cdtr_scheme_id { |
|
160 |
my ($self, $xml) = @_; |
|
161 |
|
|
162 |
$xml->startTag('CdtrSchmeId'); |
|
163 |
$xml->startTag('Id'); |
|
164 |
$xml->startTag('PrvtId'); |
|
165 |
$xml->startTag('Othr'); |
|
166 |
$xml->dataElement('Id', encode('UTF-8', substr($self->{creditor_id}, 0, 35))); |
|
167 |
$xml->startTag('SchmeNm'); |
|
168 |
$xml->dataElement('Prtry', 'SEPA'); |
|
169 |
$xml->endTag('SchmeNm'); |
|
170 |
$xml->endTag('Othr'); |
|
171 |
$xml->endTag('PrvtId'); |
|
172 |
$xml->endTag('Id'); |
|
173 |
$xml->endTag('CdtrSchmeId'); |
|
174 |
} |
|
175 |
|
|
127 | 176 |
sub to_xml { |
128 | 177 |
my $self = shift; |
129 | 178 |
|
... | ... | |
143 | 192 |
my $is_coll = $self->{collection}; |
144 | 193 |
my $cd_src = $is_coll ? 'Cdtr' : 'Dbtr'; |
145 | 194 |
my $cd_dst = $is_coll ? 'Dbtr' : 'Cdtr'; |
146 |
my $pain_id = $is_coll ? 'pain.008.001.08' : 'pain.001.001.09'; |
|
147 | 195 |
my $pain_elmt = $is_coll ? 'CstmrDrctDbtInitn' : 'CstmrCdtTrfInitn'; |
148 | 196 |
my @pii_base = (strftime('PII%Y%m%d%H%M%S', @now), rand(1000000000)); |
197 |
my $pain_id = $is_coll && ($self->{version} == V3_0_0()) ? 'pain.008.001.02' |
|
198 |
: $is_coll && ($self->{version} == V3_8_0()) ? 'pain.008.001.08' |
|
199 |
: !$is_coll && ($self->{version} == V3_0_0()) ? 'pain.001.001.03' |
|
200 |
: !$is_coll && ($self->{version} == V3_8_0()) ? 'pain.001.001.09' |
|
201 |
: die("programming error: version not handled for pain ID"); |
|
202 |
my $bic_elt = $self->{version} == V3_0_0() ? 'BIC' |
|
203 |
: $self->{version} == V3_8_0() ? 'BICFI' |
|
204 |
: die("programming error: version not handled for BIC element name"); |
|
149 | 205 |
|
150 | 206 |
my $grouped_transactions = $self->_group_transactions(); |
151 | 207 |
|
... | ... | |
194 | 250 |
} |
195 | 251 |
$xml->endTag('PmtTpInf'); |
196 | 252 |
|
197 |
if ($is_coll) { |
|
198 |
$xml->dataElement('ReqdColltnDt', $master_transaction->get('execution_date')); |
|
253 |
if ($self->{version} == V3_0_0()) { |
|
254 |
$xml->dataElement($is_coll ? 'ReqdColltnDt' : 'ReqdExctnDt', $master_transaction->get('execution_date')); |
|
255 |
|
|
256 |
} elsif ($self->{version} == V3_8_0()) { |
|
257 |
if ($is_coll) { |
|
258 |
$xml->dataElement('ReqdColltnDt', $master_transaction->get('execution_date')); |
|
259 |
} else { |
|
260 |
$xml->startTag('ReqdExctnDt'); |
|
261 |
$xml->dataElement('Dt', $master_transaction->get('execution_date')); |
|
262 |
$xml->endTag('ReqdExctnDt'); |
|
263 |
} |
|
264 |
|
|
199 | 265 |
} else { |
200 |
$xml->startTag('ReqdExctnDt'); |
|
201 |
$xml->dataElement('Dt', $master_transaction->get('execution_date')); |
|
202 |
$xml->endTag('ReqdExctnDt'); |
|
266 |
die("programming error: version not handled for ReqdColl/ExctnDt"); |
|
203 | 267 |
} |
204 | 268 |
|
205 | 269 |
$xml->startTag($cd_src); |
... | ... | |
214 | 278 |
|
215 | 279 |
$xml->startTag($cd_src . 'Agt'); |
216 | 280 |
$xml->startTag('FinInstnId'); |
217 |
$xml->dataElement('BICFI', $master_transaction->get('src_bic', 20));
|
|
281 |
$xml->dataElement($bic_elt, $master_transaction->get('src_bic', 20));
|
|
218 | 282 |
$xml->endTag('FinInstnId'); |
219 | 283 |
$xml->endTag($cd_src . 'Agt'); |
220 | 284 |
|
221 | 285 |
$xml->dataElement('ChrgBr', 'SLEV'); |
222 | 286 |
|
223 |
if ($is_coll) { |
|
224 |
$xml->startTag('CdtrSchmeId'); |
|
225 |
$xml->startTag('Id'); |
|
226 |
$xml->startTag('PrvtId'); |
|
227 |
$xml->startTag('Othr'); |
|
228 |
$xml->dataElement('Id', encode('UTF-8', substr($self->{creditor_id}, 0, 35))); |
|
229 |
$xml->startTag('SchmeNm'); |
|
230 |
$xml->dataElement('Prtry', 'SEPA'); |
|
231 |
$xml->endTag('SchmeNm'); |
|
232 |
$xml->endTag('Othr'); |
|
233 |
$xml->endTag('PrvtId'); |
|
234 |
$xml->endTag('Id'); |
|
235 |
$xml->endTag('CdtrSchmeId'); |
|
287 |
if ($is_coll && ($self->{version} == V3_8_0())) { |
|
288 |
$self->_emit_cdtr_scheme_id($xml); |
|
236 | 289 |
} |
237 | 290 |
|
238 | 291 |
foreach my $transaction (@{ $transaction_group->{transactions} }) { |
... | ... | |
253 | 306 |
$xml->dataElement('MndtId', $self->_restricted_identification_sepa2($transaction->get('mandator_id'))); |
254 | 307 |
$xml->dataElement('DtOfSgntr', $self->_restricted_identification_sepa2($transaction->get('date_of_signature'))); |
255 | 308 |
|
309 |
if ($self->{version} == V3_0_0()) { |
|
310 |
$self->_emit_cdtr_scheme_id($xml); |
|
311 |
} |
|
312 |
|
|
256 | 313 |
$xml->endTag('MndtRltdInf'); |
257 | 314 |
|
258 | 315 |
$xml->endTag('DrctDbtTx'); |
... | ... | |
267 | 324 |
|
268 | 325 |
$xml->startTag("${cd_dst}Agt"); |
269 | 326 |
$xml->startTag('FinInstnId'); |
270 |
$xml->dataElement('BICFI', $transaction->get('dst_bic', 20));
|
|
327 |
$xml->dataElement($bic_elt, $transaction->get('dst_bic', 20));
|
|
271 | 328 |
$xml->endTag('FinInstnId'); |
272 | 329 |
$xml->endTag("${cd_dst}Agt"); |
273 | 330 |
|
bin/mozilla/sepa.pl | ||
---|---|---|
198 | 198 |
'vc' => $vc); |
199 | 199 |
|
200 | 200 |
$form->header(); |
201 |
print $form->parse_html_template('sepa/bank_transfer_created', { 'id' => $id, 'vc' => $vc }); |
|
201 |
my %sepa_versions = SL::SEPA::XML->get_supported_versions; |
|
202 |
print $form->parse_html_template('sepa/bank_transfer_created', { 'id' => $id, 'vc' => $vc, sepa_versions => \%sepa_versions }); |
|
202 | 203 |
} |
203 | 204 |
|
204 | 205 |
$main::lxdebug->leave_sub(); |
... | ... | |
296 | 297 |
push @options, $form->{l_executed} ? $locale->text('executed') : $locale->text('not yet executed') if ($form->{l_executed} != $form->{l_not_executed}); |
297 | 298 |
push @options, $form->{l_closed} ? $locale->text('closed') : $locale->text('open') if ($form->{l_open} != $form->{l_closed}); |
298 | 299 |
|
300 |
my %sepa_versions = SL::SEPA::XML->get_supported_versions; |
|
301 |
|
|
299 | 302 |
$report->set_options('top_info_text' => join("\n", @options), |
300 |
'raw_top_info_text' => $form->parse_html_template('sepa/bank_transfer_list_top'), |
|
303 |
'raw_top_info_text' => $form->parse_html_template('sepa/bank_transfer_list_top', { 'show_buttons' => $open_available, sepa_versions => \%sepa_versions, }),
|
|
301 | 304 |
'raw_bottom_info_text' => $form->parse_html_template('sepa/bank_transfer_list_bottom', { 'show_buttons' => $open_available, vc => $vc }), |
302 | 305 |
'std_column_visibility' => 1, |
303 | 306 |
'output_format' => 'HTML', |
... | ... | |
553 | 556 |
'message_id' => $message_id, |
554 | 557 |
'grouped' => 1, |
555 | 558 |
'collection' => $vc eq 'customer', |
559 |
'version' => $form->{sepa_xml_version}, |
|
556 | 560 |
); |
557 | 561 |
|
558 | 562 |
foreach my $item (@items) { |
locale/de/all | ||
---|---|---|
3373 | 3373 |
'SEPA Transfer Amount' => 'Betrag in offenen SEPA Exporten', |
3374 | 3374 |
'SEPA XML Docs for Exports ' => 'SEPA-XML-Dokumente Export-Nummer', |
3375 | 3375 |
'SEPA XML download' => 'SEPA-XML-Download', |
3376 |
'SEPA XML version' => 'SEPA-XML-Version', |
|
3376 | 3377 |
'SEPA creditor ID' => 'SEPA-Kreditoren-Identifikation', |
3377 | 3378 |
'SEPA exports' => 'SEPA-Exporte', |
3378 | 3379 |
'SEPA message ID' => 'SEPA-Nachrichten-ID', |
... | ... | |
3971 | 3972 |
'The PDF has been printed' => 'Das PDF-Dokument wurde gedruckt.', |
3972 | 3973 |
'The Protocol for Host Name seems invalid (expected: http:// or https://)!' => 'Das Protokoll für den Server sieht falsch aus. Erwartet wird "http://" oder "https://".', |
3973 | 3974 |
'The Proxy Name seems invalid' => 'Der Hostname des Proxys sieht falsch aus', |
3974 |
'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt', |
|
3975 |
'The SEPA export has been created.' => 'Der SEPA-Export wurde erstellt.',
|
|
3975 | 3976 |
'The SEPA strings have been saved.' => 'Die bei SEPA-Überweisungen verwendeten Begriffe wurden gespeichert.', |
3976 | 3977 |
'The SQL query can be parameterized with variables named as follows: <%name%>.' => 'Die SQL-Abfrage kann mittels Variablen wie folgt parametrisiert werden: <%Variablenname%>.', |
3977 | 3978 |
'The SQL query does not contain any parameter that need to be configured.' => 'Die SQL-Abfrage enthält keine Parameter, die angegeben werden müssten.', |
locale/en/all | ||
---|---|---|
3372 | 3372 |
'SEPA Transfer Amount' => '', |
3373 | 3373 |
'SEPA XML Docs for Exports ' => '', |
3374 | 3374 |
'SEPA XML download' => '', |
3375 |
'SEPA XML version' => '', |
|
3375 | 3376 |
'SEPA creditor ID' => '', |
3376 | 3377 |
'SEPA exports' => '', |
3377 | 3378 |
'SEPA message ID' => '', |
templates/design40_webpages/sepa/bank_transfer_created.html | ||
---|---|---|
1 |
[% USE T8 %] |
|
2 |
[% USE HTML %] |
|
3 |
|
|
1 |
[%- USE T8 %] |
|
2 |
[% USE HTML %][%- USE LxERP -%][%- USE L -%] |
|
4 | 3 |
<h1>[% title %]</h1> |
5 | 4 |
|
6 |
<p>[% 'The SEPA export has been created.' | $T8 %]</p> |
|
7 |
<ul> |
|
8 |
<li><a href="sepa.pl?action=bank_transfer_download_sepa_xml&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> [% 'Download SEPA XML export file' | $T8 %] </a></li> |
|
9 |
[%- IF INSTANCE_CONF.get_doc_storage %] |
|
10 |
<li><a href="sepa.pl?action=bank_transfer_download_sepa_docs&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> [% 'Download Documents for exported bookings' | $T8 %] </a></li> |
|
11 |
[% END %] |
|
12 |
<li><a href="sepa.pl?action=bank_transfer_list&l_open=1&l_not_executed=1&vc=[% HTML.url(vc) %]"> [% 'List open SEPA exports' | $T8 %] </a></li> |
|
13 |
</ul> |
|
5 |
<p> |
|
6 |
[% 'The SEPA export has been created.' | $T8 %] |
|
7 |
</p> |
|
8 |
|
|
9 |
<h2>[% LxERP.t8('Download SEPA XML export file') %]</h2> |
|
10 |
|
|
11 |
<form method="post" action="sepa.pl"> |
|
12 |
<p> |
|
13 |
[% LxERP.t8("SEPA XML version") %]: |
|
14 |
[% L.select_tag('sepa_xml_version', sepa_versions.versions, title_key='description', selected=sepa_versions.default) %] |
|
15 |
</p> |
|
16 |
|
|
17 |
<p> |
|
18 |
[% L.hidden_tag('action', 'bank_transfer_download_sepa_xml') %] |
|
19 |
[% L.hidden_tag('id', id) %] |
|
20 |
[% L.hidden_tag('vc', vc) %] |
|
21 |
|
|
22 |
[% L.submit_tag('dummy', LxERP.t8('Download')) %] |
|
23 |
</p> |
|
24 |
</form> |
|
25 |
[%- IF INSTANCE_CONF.get_doc_storage %] |
|
26 |
|
|
27 |
<h2>[% LxERP.t8('Download Documents for exported bookings') %]</h2> |
|
28 |
|
|
29 |
<p> |
|
30 |
<a href="sepa.pl?action=bank_transfer_download_sepa_docs&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> [% 'Download Documents for exported bookings' | $T8 %]</a> |
|
31 |
</p> |
|
32 |
[% END %] |
|
33 |
|
|
34 |
<h2>[% LxERP.t8('List open SEPA exports') %]</h2> |
|
35 |
|
|
36 |
<p> |
|
37 |
<a href="sepa.pl?action=bank_transfer_list&l_open=1&l_not_executed=1&vc=[% HTML.url(vc) %]">[% 'List open SEPA exports' | $T8 %]</a> |
|
38 |
</p> |
templates/design40_webpages/sepa/bank_transfer_list_top.html | ||
---|---|---|
1 |
[%- USE LxERP -%][%- USE L -%] |
|
1 | 2 |
<form action="sepa.pl" method="post" id="form"> |
3 |
[% IF show_buttons %] |
|
4 |
|
|
5 |
<p> |
|
6 |
[% LxERP.t8("SEPA XML version") %]: |
|
7 |
[% L.select_tag('sepa_xml_version', sepa_versions.versions, title_key='description', selected=sepa_versions.default) %] |
|
8 |
</p> |
|
9 |
[% END %] |
templates/webpages/sepa/bank_transfer_created.html | ||
---|---|---|
1 | 1 |
[%- USE T8 %] |
2 |
[% USE HTML %] |
|
2 |
[% USE HTML %][%- USE LxERP -%][%- USE L -%]
|
|
3 | 3 |
<h1>[% title %]</h1> |
4 | 4 |
|
5 |
<p>
|
|
5 |
<p> |
|
6 | 6 |
[% 'The SEPA export has been created.' | $T8 %] |
7 |
</p> |
|
8 |
|
|
9 |
<p> |
|
10 |
<ul> |
|
11 |
<li> |
|
12 |
<a href="sepa.pl?action=bank_transfer_download_sepa_xml&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> |
|
13 |
[% 'Download SEPA XML export file' | $T8 %] |
|
14 |
</a> |
|
15 |
</li> |
|
16 |
[%- IF INSTANCE_CONF.get_doc_storage %] |
|
17 |
<li><a href="sepa.pl?action=bank_transfer_download_sepa_docs&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> [% 'Download Documents for exported bookings' | $T8 %] </a></li> |
|
18 |
[% END %] |
|
19 |
<li> |
|
20 |
<a href="sepa.pl?action=bank_transfer_list&l_open=1&l_not_executed=1&vc=[% HTML.url(vc) %]"> |
|
21 |
[% 'List open SEPA exports' | $T8 %] |
|
22 |
</a> |
|
23 |
</li> |
|
24 |
</ul> |
|
25 |
</p> |
|
7 |
</p> |
|
26 | 8 |
|
9 |
<h2>[% LxERP.t8('Download SEPA XML export file') %]</h2> |
|
10 |
|
|
11 |
<form method="post" action="sepa.pl"> |
|
12 |
<p> |
|
13 |
[% LxERP.t8("SEPA XML version") %]: |
|
14 |
[% L.select_tag('sepa_xml_version', sepa_versions.versions, title_key='description', selected=sepa_versions.default) %] |
|
15 |
</p> |
|
16 |
|
|
17 |
<p> |
|
18 |
[% L.hidden_tag('action', 'bank_transfer_download_sepa_xml') %] |
|
19 |
[% L.hidden_tag('id', id) %] |
|
20 |
[% L.hidden_tag('vc', vc) %] |
|
21 |
|
|
22 |
[% L.submit_tag('dummy', LxERP.t8('Download')) %] |
|
23 |
</p> |
|
24 |
</form> |
|
25 |
[%- IF INSTANCE_CONF.get_doc_storage %] |
|
26 |
|
|
27 |
<h2>[% LxERP.t8('Download Documents for exported bookings') %]</h2> |
|
28 |
|
|
29 |
<p> |
|
30 |
<a href="sepa.pl?action=bank_transfer_download_sepa_docs&id=[% HTML.url(id) %]&vc=[% HTML.url(vc) %]"> [% 'Download Documents for exported bookings' | $T8 %]</a> |
|
31 |
</p> |
|
32 |
[% END %] |
|
33 |
|
|
34 |
<h2>[% LxERP.t8('List open SEPA exports') %]</h2> |
|
35 |
|
|
36 |
<p> |
|
37 |
<a href="sepa.pl?action=bank_transfer_list&l_open=1&l_not_executed=1&vc=[% HTML.url(vc) %]">[% 'List open SEPA exports' | $T8 %]</a> |
|
38 |
</p> |
templates/webpages/sepa/bank_transfer_list_top.html | ||
---|---|---|
1 |
[%- USE LxERP -%][%- USE L -%] |
|
1 | 2 |
<form action="sepa.pl" method="post" id="form"> |
3 |
[% IF show_buttons %] |
|
4 |
|
|
5 |
<p> |
|
6 |
[% LxERP.t8("SEPA XML version") %]: |
|
7 |
[% L.select_tag('sepa_xml_version', sepa_versions.versions, title_key='description', selected=sepa_versions.default) %] |
|
8 |
</p> |
|
9 |
[% END %] |
Auch abrufbar als: Unified diff
SEPA: XML-Version beim Download auswählbar