Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision cf0455f5

Von Moritz Bunkus vor fast 5 Jahren hinzugefügt

  • ID cf0455f57e7fe398b59a3c4a01f459d16b9db419
  • Vorgänger fd7e51e9
  • Nachfolger e5f53eb5

ZUGFeRD: Rechnungen mit ZUGFeRD-Daten erzeugen

Unterschiede anzeigen:

SL/DB/Helper/PDF_A.pm
57 57
        author   => $author,
58 58
        language => $pdf_language,
59 59
      },
60
      zugferd              => {
61
        conformance_level  => 'EXTENDED',
62
        document_file_name => 'ZUGFeRD-invoice.xml',
63
        document_type      => 'INVOICE',
64
        version            => '1.0',
65
      },
60 66
    ),
61 67
  };
62 68
}
SL/DB/Helper/ZUGFeRD.pm
1
package SL::DB::Helper::ZUGFeRD;
2

  
3
use strict;
4
use utf8;
5

  
6
use parent qw(Exporter);
7
our @EXPORT = qw(create_zugferd_data create_zugferd_xmp_data);
8

  
9
use SL::DB::Tax;
10
use SL::DB::TaxKey;
11
use SL::Helper::ISO3166;
12
use SL::Helper::ISO4217;
13
use SL::Helper::UNECERecommendation20;
14

  
15
use Carp;
16
use Encode qw(encode);
17
use List::MoreUtils qw(pairwise);
18
use List::Util qw(sum);
19
use Template;
20
use XML::Writer;
21

  
22
my @line_names = qw(LineOne LineTwo LineThree);
23

  
24
sub _u8 {
25
  my ($value) = @_;
26
  return encode('UTF-8', $value // '');
27
}
28

  
29
sub _r2 {
30
  my ($value) = @_;
31
  return $::form->round_amount($value, 2);
32
}
33

  
34
sub _type_name {
35
  my ($self) = @_;
36
  my $type   = $self->invoice_type;
37

  
38
  no warnings 'once';
39
  return $type eq 'ar_transaction' ? $::locale->text('Invoice') : $self->displayable_type;
40
}
41

  
42
sub _type_code {
43
  my ($self) = @_;
44
  my $type   = $self->invoice_type;
45

  
46
  # 326 (Partial invoice)
47
  # 380 (Commercial invoice)
48
  # 384 (Corrected Invoice)
49
  # 381 (Credit note)
50
  # 389 (Credit note, self billed invoice)
51

  
52
  return $type eq 'credit_note' ? 381 : 380;
53
}
54

  
55
sub _unit_code {
56
  my ($unit) = @_;
57

  
58
  # Mapping from kivitendo's units to UN/ECE Recommendation 20 & 21.
59
  my $code = SL::Helper::UNECERecommendation20::map_name_to_code($unit);
60
  return $code if $code;
61

  
62
  $::lxdebug->message(LXDebug::WARN(), "ZUGFeRD unit name mapping: no UN/ECE Recommendation 20/21 unit known for kivitendo unit '$unit'; using 'C62'");
63

  
64
  return 'C62';
65
}
66

  
67
sub _parse_our_address {
68
  my @result;
69
  my @street = grep { $_ } ($::instance_conf->get_address_street1, $::instance_conf->get_address_street2);
70

  
71
  push @result, [ 'PostcodeCode', $::instance_conf->get_address_zipcode ] if $::instance_conf->get_address_zipcode;
72
  push @result, grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @street;
73
  push @result, [ 'CityName', $::instance_conf->get_address_city ] if $::instance_conf->get_address_city;
74
  push @result, [ 'CountryID', SL::Helper::ISO3166::map_name_to_alpha_2_code($::instance_conf->get_address_country) // 'DE' ];
75

  
76
  return @result;
77
}
78

  
79
sub _customer_postal_trade_address {
80
  my (%params) = @_;
81

  
82
  #       <ram:PostalTradeAddress>
83
  $params{xml}->startTag("ram:PostalTradeAddress");
84

  
85
  my @parts = grep { $_ } map { $params{customer}->$_ } qw(department_1 department_2 street);
86

  
87
  $params{xml}->dataElement("ram:PostcodeCode", _u8($params{customer}->zipcode));
88
  $params{xml}->dataElement("ram:" . $_->[0],   _u8($_->[1])) for grep { $_->[1] } pairwise { [ $a, $b] } @line_names, @parts;
89
  $params{xml}->dataElement("ram:CityName",     _u8($params{customer}->city));
90
  $params{xml}->dataElement("ram:CountryID",    _u8(SL::Helper::ISO3166::map_name_to_alpha_2_code($params{customer}->country) // 'DE'));
91
  $params{xml}->endTag;
92
  #       </ram:PostalTradeAddress>
93
}
94

  
95
sub _tax_rate_and_code {
96
  my ($taxzone, $tax) = @_;
97

  
98
  my ($tax_rate, $tax_code) = @_;
99

  
100
  if ($taxzone->description =~ m{Au.*erhalb}) {
101
    $tax_rate = 0;
102
    $tax_code = 'G';
103

  
104
  } elsif ($taxzone->description =~ m{EU mit}) {
105
    $tax_rate = 0;
106
    $tax_code = 'K';
107

  
108
  } else {
109
    $tax_rate = $tax->rate * 100;
110
    $tax_code = !$tax_rate ? 'Z' : 'S';
111
  }
112

  
113
  return (rate => $tax_rate, code => $tax_code);
114
}
115

  
116
sub _line_item {
117
  my ($self, %params) = @_;
118

  
119
  my $item_ptc = $params{ptc_data}->{items}->[$params{line_number}];
120

  
121
  my $taxkey   = $item_ptc->{taxkey_id} ? SL::DB::TaxKey->load_cached($item_ptc->{taxkey_id}) : undef;
122
  my $tax      = $item_ptc->{taxkey_id} ? SL::DB::Tax->load_cached($taxkey->tax_id)           : undef;
123
  my %tax_info = _tax_rate_and_code($self->taxzone, $tax);
124

  
125
  # <ram:IncludedSupplyChainTradeLineItem>
126
  $params{xml}->startTag("ram:IncludedSupplyChainTradeLineItem");
127

  
128
  #   <ram:AssociatedDocumentLineDocument>
129
  $params{xml}->startTag("ram:AssociatedDocumentLineDocument");
130
  $params{xml}->dataElement("ram:LineID", $params{line_number} + 1);
131
  $params{xml}->endTag;
132

  
133
  $params{xml}->startTag("ram:SpecifiedTradeProduct");
134
  $params{xml}->dataElement("ram:SellerAssignedID", _u8($params{item}->part->partnumber));
135
  $params{xml}->dataElement("ram:Name",             _u8($params{item}->description));
136
  $params{xml}->endTag;
137

  
138
  $params{xml}->startTag("ram:SpecifiedLineTradeAgreement");
139
  $params{xml}->startTag("ram:NetPriceProductTradePrice");
140
  $params{xml}->dataElement("ram:ChargeAmount", _r2($item_ptc->{sellprice}));
141
  $params{xml}->endTag;
142
  $params{xml}->endTag;
143
  #   </ram:SpecifiedLineTradeAgreement>
144

  
145
  #   <ram:SpecifiedLineTradeDelivery>
146
  $params{xml}->startTag("ram:SpecifiedLineTradeDelivery");
147
  $params{xml}->dataElement("ram:BilledQuantity", $params{item}->qty, "unitCode" => _unit_code($params{item}->unit));
148
  $params{xml}->endTag;
149
  #   </ram:SpecifiedLineTradeDelivery>
150

  
151
  #   <ram:SpecifiedLineTradeSettlement>
152
  $params{xml}->startTag("ram:SpecifiedLineTradeSettlement");
153

  
154
  #     <ram:ApplicableTradeTax>
155
  $params{xml}->startTag("ram:ApplicableTradeTax");
156
  $params{xml}->dataElement("ram:TypeCode",              "VAT");
157
  $params{xml}->dataElement("ram:CategoryCode",          $tax_info{code});
158
  $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate}));
159
  $params{xml}->endTag;
160
  #     </ram:ApplicableTradeTax>
161

  
162
  #     <ram:SpecifiedTradeSettlementLineMonetarySummation>
163
  $params{xml}->startTag("ram:SpecifiedTradeSettlementLineMonetarySummation");
164
  $params{xml}->dataElement("ram:LineTotalAmount", _r2($item_ptc->{linetotal}));
165
  $params{xml}->endTag;
166
  #     </ram:SpecifiedTradeSettlementLineMonetarySummation>
167

  
168
  $params{xml}->endTag;
169
  #   </ram:SpecifiedLineTradeSettlement>
170

  
171
  $params{xml}->endTag;
172
  # <ram:IncludedSupplyChainTradeLineItem>
173
}
174

  
175
sub _taxes {
176
  my ($self, %params) = @_;
177

  
178
  my %taxkey_info;
179

  
180
  foreach my $item (@{ $params{ptc_data}->{items} }) {
181
    $taxkey_info{$item->{taxkey_id}} //= {
182
      linetotal  => 0,
183
      tax_amount => 0,
184
    };
185
    my $info             = $taxkey_info{$item->{taxkey_id}};
186
    $info->{taxkey}    //= SL::DB::TaxKey->load_cached($item->{taxkey_id});
187
    $info->{tax}       //= SL::DB::Tax->load_cached($info->{taxkey}->tax_id);
188
    $info->{linetotal}  += $item->{linetotal};
189
    $info->{tax_amount} += $item->{tax_amount};
190
  }
191

  
192
  foreach my $taxkey_id (sort keys %taxkey_info) {
193
    my $info     = $taxkey_info{$taxkey_id};
194
    my %tax_info = _tax_rate_and_code($self->taxzone, $info->{tax});
195

  
196
    #     <ram:ApplicableTradeTax>
197
    $params{xml}->startTag("ram:ApplicableTradeTax");
198
    $params{xml}->dataElement("ram:CalculatedAmount",      _r2($params{ptc_data}->{taxes}->{$info->{tax}->{chart_id}}));
199
    $params{xml}->dataElement("ram:TypeCode",              "VAT");
200
    $params{xml}->dataElement("ram:BasisAmount",           _r2($info->{linetotal}));
201
    $params{xml}->dataElement("ram:CategoryCode",          $tax_info{code});
202
    $params{xml}->dataElement("ram:RateApplicablePercent", _r2($tax_info{rate}));
203
    $params{xml}->endTag;
204
    #     </ram:ApplicableTradeTax>
205
  }
206
}
207

  
208
sub _format_payment_terms_description {
209
  my ($self) = @_;
210

  
211
  my (%vars, %amounts, %formatted_amounts);
212

  
213
  local $::myconfig{numberformat} = $::myconfig{numberformat};
214
  local $::myconfig{dateformat}   = $::myconfig{dateformat};
215

  
216
  if ($self->language_id) {
217
    my $language = SL::DB::Language->load_cached($self->language_id);
218
    $::myconfig{dateformat}   = $language->output_dateformat   if $language->output_dateformat;
219
    $::myconfig{numberformat} = $language->output_numberformat if $language->output_numberformat;
220
  }
221

  
222
  $vars{currency}              = $self->currency->name if $self->currency;
223
  $vars{$_}                    = $self->customer->$_      for qw(account_number bank bank_code bic iban mandate_date_of_signature mandator_id);
224
  $vars{$_}                    = $self->payment_terms->$_ for qw(terms_netto terms_skonto percent_skonto);
225
  $vars{payment_description}   = $self->payment_terms->description;
226
  $vars{netto_date}            = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'net')->to_kivitendo;
227
  $vars{skonto_date}           = $self->payment_terms->calc_date(reference_date => $self->transdate, due_date => $self->duedate, terms => 'discount')->to_kivitendo;
228

  
229
  $amounts{invtotal}           = $self->amount;
230
  $amounts{total}              = $self->amount - $self->paid;
231

  
232
  $amounts{skonto_in_percent}  = 100.0 * $vars{percent_skonto};
233
  $amounts{skonto_amount}      = $amounts{invtotal} * $vars{percent_skonto};
234
  $amounts{invtotal_wo_skonto} = $amounts{invtotal} * (1 - $vars{percent_skonto});
235
  $amounts{total_wo_skonto}    = $amounts{total}    * (1 - $vars{percent_skonto});
236

  
237
  foreach (keys %amounts) {
238
    $amounts{$_}           = $::form->round_amount($amounts{$_}, 2);
239
    $formatted_amounts{$_} = $::form->format_amount(\%::myconfig, $amounts{$_}, 2);
240
  }
241

  
242
  my $description = ($self->payment_terms->translated_attribute('description_long_invoice', $self->language_id) // '') || $self->payment_terms->description_long_invoice;
243
  $description    =~ s{<\%$_\%>}{ $vars{$_} }ge              for keys %vars;
244
  $description    =~ s{<\%$_\%>}{ $formatted_amounts{$_} }ge for keys %formatted_amounts;
245

  
246
  return $description;
247
}
248

  
249
sub _payment_terms {
250
  my ($self, %params) = @_;
251

  
252
  return unless $self->payment_terms;
253

  
254
  #     <ram:SpecifiedTradePaymentTerms>
255
  $params{xml}->startTag("ram:SpecifiedTradePaymentTerms");
256

  
257
  $params{xml}->dataElement("ram:Description", _u8(_format_payment_terms_description($self)));
258

  
259
  #       <ram:DueDateDateTime>
260
  $params{xml}->startTag("ram:DueDateDateTime");
261
  $params{xml}->dataElement("udt:DateTimeString", $self->duedate->strftime('%Y%m%d'), "format" => "102");
262
  $params{xml}->endTag;
263
  #       </ram:DueDateDateTime>
264

  
265
  if ($self->payment_terms->percent_skonto && $self->payment_terms->terms_skonto) {
266
    #       <ram:ApplicableTradePaymentDiscountTerms>
267
    $params{xml}->startTag("ram:ApplicableTradePaymentDiscountTerms");
268
    $params{xml}->dataElement("ram:BasisPeriodMeasure", $self->payment_terms->terms_skonto, "unitCode" => "DAY");
269
    $params{xml}->dataElement("ram:CalculationPercent", _r2($self->payment_terms->percent_skonto * 100));
270
    $params{xml}->endTag;
271
    #       </ram:ApplicableTradePaymentDiscountTerms>
272
  }
273

  
274
  $params{xml}->endTag;
275
  #     </ram:SpecifiedTradePaymentTerms>
276
}
277

  
278
sub _totals {
279
  my ($self, %params) = @_;
280

  
281
  #     <ram:SpecifiedTradeSettlementHeaderMonetarySummation>
282
  $params{xml}->startTag("ram:SpecifiedTradeSettlementHeaderMonetarySummation");
283

  
284
  $params{xml}->dataElement("ram:LineTotalAmount",     _r2($self->netamount));
285
  $params{xml}->dataElement("ram:TaxBasisTotalAmount", _r2($self->netamount));
286
  $params{xml}->dataElement("ram:TaxTotalAmount",      _r2(sum(values %{ $params{ptc_data}->{taxes} })), "currencyID" => "EUR");
287
  $params{xml}->dataElement("ram:GrandTotalAmount",    _r2($self->amount));
288
  $params{xml}->dataElement("ram:TotalPrepaidAmount",  _r2($self->paid));
289
  $params{xml}->dataElement("ram:DuePayableAmount",    _r2($self->amount - $self->paid));
290

  
291
  $params{xml}->endTag;
292
  #     </ram:SpecifiedTradeSettlementHeaderMonetarySummation>
293
}
294

  
295
sub _exchanged_document_context {
296
  my ($self, %params) = @_;
297

  
298
  #   <rsm:ExchangedDocumentContext>
299
  $params{xml}->startTag("rsm:ExchangedDocumentContext");
300
  $params{xml}->startTag("ram:TestIndicator");
301
  $params{xml}->dataElement("udt:Indicator", "true"); # TODO: change to 'false'
302
  $params{xml}->endTag;
303

  
304
  $params{xml}->startTag("ram:GuidelineSpecifiedDocumentContextParameter");
305
  $params{xml}->dataElement("ram:ID", "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended");
306
  $params{xml}->endTag;
307
  $params{xml}->endTag;
308
  #   </rsm:ExchangedDocumentContext>
309
}
310

  
311
sub _exchanged_document {
312
  my ($self, %params) = @_;
313

  
314
  #   <rsm:ExchangedDocument>
315
  $params{xml}->startTag("rsm:ExchangedDocument");
316

  
317
  $params{xml}->dataElement("ram:ID",       _u8($self->invnumber));
318
  $params{xml}->dataElement("ram:Name",     _u8(_type_name($self)));
319
  $params{xml}->dataElement("ram:TypeCode", _u8(_type_code($self)));
320

  
321
  #     <ram:IssueDateTime>
322
  $params{xml}->startTag("ram:IssueDateTime");
323
  $params{xml}->dataElement("udt:DateTimeString", $self->transdate->strftime('%Y%m%d'), "format" => "102");
324
  $params{xml}->endTag;
325
  #     </ram:IssueDateTime>
326

  
327
  if ($self->language && (($self->language->template_code // '') =~ m{^(de|en)}i)) {
328
    $params{xml}->dataElement("ram:LanguageID", uc($1));
329
  }
330

  
331
  if ($self->transaction_description) {
332
    $params{xml}->startTag("ram:IncludedNote");
333
    $params{xml}->dataElement("ram:Content", _u8($self->transaction_description));
334
    $params{xml}->endTag;
335
  }
336

  
337
  my $notes = $self->notes_as_stripped_html;
338
  if ($notes) {
339
    $params{xml}->startTag("ram:IncludedNote");
340
    $params{xml}->dataElement("ram:Content", _u8($notes));
341
    $params{xml}->endTag;
342
  }
343

  
344
  $params{xml}->endTag;
345
  #   </rsm:ExchangedDocument>
346
}
347

  
348
sub _seller_trade_party {
349
  my ($self, %params) = @_;
350

  
351
  my @our_address            = _parse_our_address();
352

  
353
  my $sales_person           = $self->salesman;
354
  my $sales_person_auth      = SL::DB::Manager::AuthUser->find_by(login => $sales_person->login);
355
  my %sales_person_cfg       = $sales_person_auth ? %{ $sales_person_auth->config_values } : ();
356
  $sales_person_cfg{email} ||= $sales_person->deleted_email;
357
  $sales_person_cfg{tel}   ||= $sales_person->deleted_tel;
358

  
359
  #       <ram:SellerTradeParty>
360
  $params{xml}->startTag("ram:SellerTradeParty");
361
  $params{xml}->dataElement("ram:ID",   _u8($self->customer->c_vendor_id)) if ($self->customer->c_vendor_id // '') ne '';
362
  $params{xml}->dataElement("ram:Name", _u8($::instance_conf->get_company));
363

  
364
  #         <ram:DefinedTradeContact>
365
  $params{xml}->startTag("ram:DefinedTradeContact");
366

  
367
  $params{xml}->dataElement("ram:PersonName", _u8($sales_person_cfg{name} || $sales_person_cfg{login}));
368

  
369
  if ($sales_person_cfg{tel}) {
370
    $params{xml}->startTag("ram:TelephoneUniversalCommunication");
371
    $params{xml}->dataElement("ram:CompleteNumber", _u8($sales_person_cfg{tel}));
372
    $params{xml}->endTag;
373
  }
374

  
375
  if ($sales_person_cfg{email}) {
376
    $params{xml}->startTag("ram:EmailURIUniversalCommunication");
377
    $params{xml}->dataElement("ram:URIID", _u8($sales_person_cfg{email}));
378
    $params{xml}->endTag;
379
  }
380

  
381
  $params{xml}->endTag;
382
  #         </ram:DefinedTradeContact>
383

  
384
  if (@our_address) {
385
    #         <ram:PostalTradeAddress>
386
    $params{xml}->startTag("ram:PostalTradeAddress");
387
    foreach my $element (@our_address) {
388
      $params{xml}->dataElement("ram:" . $element->[0], _u8($element->[1]));
389
    }
390
    $params{xml}->endTag;
391
    #         </ram:PostalTradeAddress>
392
  }
393

  
394
  my $ustid_nr = $::instance_conf->get_co_ustid;
395
  if ($ustid_nr) {
396
    $ustid_nr = "DE$ustid_nr" unless $ustid_nr =~ m{^[A-Z]{2}};
397
    #         <ram:SpecifiedTaxRegistration>
398
    $params{xml}->startTag("ram:SpecifiedTaxRegistration");
399
    $params{xml}->dataElement("ram:ID", _u8($ustid_nr), "schemeID" => "VA");
400
    $params{xml}->endTag;
401
    #         </ram:SpecifiedTaxRegistration>
402
  }
403

  
404
  $params{xml}->endTag;
405
  #     </ram:SellerTradeParty>
406
}
407

  
408
sub _buyer_trade_party {
409
  my ($self, %params) = @_;
410

  
411
  #       <ram:BuyerTradeParty>
412
  $params{xml}->startTag("ram:BuyerTradeParty");
413
  $params{xml}->dataElement("ram:ID",   _u8($self->customer->customernumber));
414
  $params{xml}->dataElement("ram:Name", _u8($self->customer->name));
415

  
416
  _customer_postal_trade_address(%params, customer => $self->customer);
417

  
418
  $params{xml}->endTag;
419
  #       </ram:BuyerTradeParty>
420
}
421

  
422
sub _included_supply_chain_trade_line_item {
423
  my ($self, %params) = @_;
424

  
425
  my $line_number = 0;
426
  foreach my $item (@{ $self->items }) {
427
    _line_item($self, %params, item => $item, line_number => $line_number);
428
    $line_number++;
429
  }
430
}
431

  
432
sub _applicable_header_trade_agreement {
433
  my ($self, %params) = @_;
434

  
435
  #     <ram:ApplicableHeaderTradeAgreement>
436
  $params{xml}->startTag("ram:ApplicableHeaderTradeAgreement");
437

  
438
  _seller_trade_party($self, %params);
439
  _buyer_trade_party($self, %params);
440

  
441
  if ($self->cusordnumber) {
442
    #     <ram:BuyerOrderReferencedDocument>
443
    $params{xml}->startTag("ram:BuyerOrderReferencedDocument");
444
    $params{xml}->dataElement("ram:IssuerAssignedID", _u8($self->cusordnumber));
445
    $params{xml}->endTag;
446
    #     </ram:BuyerOrderReferencedDocument>
447
  }
448

  
449
  $params{xml}->endTag;
450
  #     </ram:ApplicableHeaderTradeAgreement>
451
}
452

  
453
sub _applicable_header_trade_delivery {
454
  my ($self, %params) = @_;
455

  
456
  #     <ram:ApplicableHeaderTradeDelivery>
457
  $params{xml}->startTag("ram:ApplicableHeaderTradeDelivery");
458
  #       <ram:ActualDeliverySupplyChainEvent>
459
  $params{xml}->startTag("ram:ActualDeliverySupplyChainEvent");
460

  
461
  $params{xml}->startTag("ram:OccurrenceDateTime");
462
  $params{xml}->dataElement("udt:DateTimeString", ($self->deliverydate // $self->transdate)->strftime('%Y%m%d'), "format" => "102");
463
  $params{xml}->endTag;
464

  
465
  $params{xml}->endTag;
466
  #       </ram:ActualDeliverySupplyChainEvent>
467
  $params{xml}->endTag;
468
  #     </ram:ApplicableHeaderTradeDelivery>
469
}
470

  
471
sub _applicable_header_trade_settlement {
472
  my ($self, %params) = @_;
473

  
474
  #     <ram:ApplicableHeaderTradeSettlement>
475
  $params{xml}->startTag("ram:ApplicableHeaderTradeSettlement");
476
  $params{xml}->dataElement("ram:InvoiceCurrencyCode", _u8(SL::Helper::ISO4217::map_currency_name_to_code($self->currency->name) // 'EUR'));
477

  
478
  _taxes($self, %params);
479
  _payment_terms($self, %params);
480
  _totals($self, %params);
481

  
482
  $params{xml}->endTag;
483
  #     </ram:ApplicableHeaderTradeSettlement>
484
}
485

  
486
sub _supply_chain_trade_transaction {
487
  my ($self, %params) = @_;
488

  
489
  #   <rsm:SupplyChainTradeTransaction>
490
  $params{xml}->startTag("rsm:SupplyChainTradeTransaction");
491

  
492
  _included_supply_chain_trade_line_item($self, %params);
493
  _applicable_header_trade_agreement($self, %params);
494
  _applicable_header_trade_delivery($self, %params);
495
  _applicable_header_trade_settlement($self, %params);
496

  
497
  $params{xml}->endTag;
498
  #   </rsm:SupplyChainTradeTransaction>
499
}
500

  
501
sub create_zugferd_data {
502
  my ($self) = @_;
503

  
504
  my %ptc_data = $self->calculate_prices_and_taxes;
505
  my $output   = '';
506
  my $xml      = XML::Writer->new(
507
    OUTPUT      => \$output,
508
    DATA_MODE   => 1,
509
    DATA_INDENT => 2,
510
    ENCODING    => 'utf-8',
511
  );
512

  
513
  my %params = (
514
    ptc_data => \%ptc_data,
515
    xml      => $xml,
516
  );
517

  
518
  $xml->xmlDecl();
519

  
520
  # <rsm:CrossIndustryInvoice>
521
  $xml->startTag("rsm:CrossIndustryInvoice",
522
                 "xmlns:a"   => "urn:un:unece:uncefact:data:standard:QualifiedDataType:100",
523
                 "xmlns:rsm" => "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100",
524
                 "xmlns:qdt" => "urn:un:unece:uncefact:data:standard:QualifiedDataType:10",
525
                 "xmlns:ram" => "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100",
526
                 "xmlns:xs"  => "http://www.w3.org/2001/XMLSchema",
527
                 "xmlns:udt" => "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100");
528

  
529
  _exchanged_document_context($self, %params);
530
  _exchanged_document($self, %params);
531
  _supply_chain_trade_transaction($self, %params);
532

  
533
  $xml->endTag;
534
  # </rsm:CrossIndustryInvoice>
535

  
536
  return $output;
537
}
538

  
539
sub create_zugferd_xmp_data {
540
  my ($self) = @_;
541

  
542
  return {
543
    conformance_level  => 'EXTENDED',
544
    document_file_name => 'ZUGFeRD-invoice.xml',
545
    document_type      => 'INVOICE',
546
    version            => '1.0',
547
  };
548
}
549

  
550
1;
SL/DB/Invoice.pm
17 17
use SL::DB::Helper::PriceTaxCalculator;
18 18
use SL::DB::Helper::PriceUpdater;
19 19
use SL::DB::Helper::TransNumberGenerator;
20
use SL::DB::Helper::ZUGFeRD;
20 21
use SL::Locale::String qw(t8);
21 22
use SL::DB::CustomVariable;
22 23

  
bin/mozilla/io.pl
1288 1288
    $form->{TEMPLATE_DRIVER_OPTIONS}->{variable_content_types} = $form->get_variable_content_types();
1289 1289
  }
1290 1290

  
1291
  if ($form->{format} =~ m{pdf}) {
1292
    _maybe_attach_zugferd_data($form);
1293
  }
1294

  
1291 1295
  $form->isblank("email", $locale->text('E-mail address missing!'))
1292 1296
    if ($form->{media} eq 'email');
1293 1297
  $form->isblank("${inv}date",
......
2110 2114

  
2111 2115
  print $::form->redirect_header($script . '?action=edit&id=' . $::form->escape($id) . '&type=' . $::form->escape($type));
2112 2116
}
2117

  
2118
sub _maybe_attach_zugferd_data {
2119
  my ($form) = @_;
2120

  
2121
  my $record = _make_record();
2122

  
2123
  return if !$record || !$record->can('create_pdf_a_print_options') || !$record->can('create_zugferd_data');
2124

  
2125
  my $xmlfile = File::Temp->new;
2126
  $xmlfile->print($record->create_zugferd_data);
2127
  $xmlfile->close;
2128

  
2129
  $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_a}           = $record->create_pdf_a_print_options(zugferd_xmp_data => $record->create_zugferd_xmp_data);
2130
  $form->{TEMPLATE_DRIVER_OPTIONS}->{pdf_attachments} = [
2131
    { source       => $xmlfile,
2132
      name         => 'ZUGFeRD-invoice.xml',
2133
      description  => $::locale->text('ZUGFeRD invoice'),
2134
      relationship => '/Alternative',
2135
      mime_type    => 'text/xml',
2136
    }
2137
  ];
2138
}
locale/de/all
3978 3978
  'Your download does not exist anymore. Please re-run the DATEV export assistant.' => 'Ihr Download existiert nicht mehr. Bitte starten Sie den DATEV-Exportassistenten erneut.',
3979 3979
  'Your import is being processed.' => 'Ihr Import wird verarbeitet',
3980 3980
  'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.',
3981
  'ZUGFeRD invoice'             => 'ZUGFeRD-Rechnung',
3981 3982
  'Zeitraum'                    => 'Zeitraum',
3982 3983
  'Zero amount posting!'        => 'Buchung ohne Wert',
3983 3984
  'Zip'                         => 'PLZ',
templates/pdf/pdf_a_metadata.xmp
1 1
<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d' ?>
2 2

  
3
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 4.0-c316 44.253921, Sun Oct 01 2006 17:14:39">
3
<x:xmpmeta xmlns:x="adobe:ns:meta/"
4
           x:xmptk="Adobe XMP Core 4.0-c316 44.253921, Sun Oct 01 2006 17:14:39">
4 5
 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
5 6
  <rdf:Description rdf:about=""
6 7
                   xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
......
54 55
       </rdf:li>
55 56
      </rdf:Seq></pdfaSchema:property>
56 57
     </rdf:li>
58
[% IF zugferd %]
59
     <rdf:li rdf:parseType="Resource">
60
      <pdfaSchema:schema>ZUGFeRD PDFA Extension Schema</pdfaSchema:schema>
61
      <pdfaSchema:namespaceURI>urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#</pdfaSchema:namespaceURI>
62
      <pdfaSchema:prefix>fx</pdfaSchema:prefix>
63
      <pdfaSchema:property>
64
       <rdf:Seq>
65
        <rdf:li rdf:parseType="Resource">
66
         <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
67
         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
68
         <pdfaProperty:category>external</pdfaProperty:category>
69
         <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
70
        </rdf:li>
71
        <rdf:li rdf:parseType="Resource">
72
         <pdfaProperty:name>DocumentType</pdfaProperty:name>
73
         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
74
         <pdfaProperty:category>external</pdfaProperty:category>
75
         <pdfaProperty:description>INVOICE</pdfaProperty:description>
76
        </rdf:li>
77
        <rdf:li rdf:parseType="Resource">
78
         <pdfaProperty:name>Version</pdfaProperty:name>
79
         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
80
         <pdfaProperty:category>external</pdfaProperty:category>
81
         <pdfaProperty:description>The actual version of the ZUGFeRD data</pdfaProperty:description>
82
        </rdf:li>
83
        <rdf:li rdf:parseType="Resource">
84
         <pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
85
         <pdfaProperty:valueType>Text</pdfaProperty:valueType>
86
         <pdfaProperty:category>external</pdfaProperty:category>
87
         <pdfaProperty:description>The conformance level of the ZUGFeRD data</pdfaProperty:description>
88
        </rdf:li>
89
       </rdf:Seq>
90
      </pdfaSchema:property>
91
     </rdf:li>
92
[% END %]
57 93
    </rdf:Bag>
58 94
   </pdfaExtension:schemas>
59 95
  </rdf:Description>
......
86 122
  </rdf:Description>
87 123
  <rdf:Description rdf:about="" xmlns:xmpRights = "http://ns.adobe.com/xap/1.0/rights/">
88 124
  </rdf:Description>
125

  
126
[% IF zugferd %]
127
  <rdf:Description xmlns:fx="urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#"
128
                   fx:ConformanceLevel="[% zugferd.conformance_level | xml %]"
129
                   fx:DocumentFileName="[% zugferd.document_file_name | xml %]"
130
                   fx:DocumentType="[% zugferd.document_type | xml %]"
131
                   fx:Version="[% zugferd.version %]"
132
                   rdf:about=""/>
133
[% END %]
134

  
89 135
 </rdf:RDF>
90 136
</x:xmpmeta>
91 137

  

Auch abrufbar als: Unified diff