Fixes for zugferd standard

This commit is contained in:
David Bomba 2025-03-04 13:41:59 +11:00
parent 168cd7add0
commit bdb3676577
5 changed files with 152 additions and 27 deletions

View File

@ -333,7 +333,7 @@ class BaseRule implements RuleInterface
} }
public function defaultForeign(): self public function defaultForeign(): self
{ {nlog("default foreign");
if ($this->invoice->client->is_tax_exempt) { if ($this->invoice->client->is_tax_exempt) {
$this->tax_rate1 = 0; $this->tax_rate1 = 0;

View File

@ -73,6 +73,7 @@ class InvoiceItemFactory
$item->tax_name1 = 'GST'; $item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00; $item->tax_rate1 = 10.00;
$item->type_id = '1'; $item->type_id = '1';
$item->tax_id = '1';
$data[] = $item; $data[] = $item;
} }
@ -93,6 +94,7 @@ class InvoiceItemFactory
$item->tax_name1 = 'GST'; $item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00; $item->tax_rate1 = 10.00;
$item->type_id = '2'; $item->type_id = '2';
$item->tax_id = '2';
$data[] = $item; $data[] = $item;
@ -127,7 +129,7 @@ class InvoiceItemFactory
$item->tax_name1 = ''; $item->tax_name1 = '';
$item->tax_rate1 = 0; $item->tax_rate1 = 0;
$item->type_id = '1'; $item->type_id = '1';
$item->tax_id = '1';
$data[] = $item; $data[] = $item;
} }

View File

@ -88,12 +88,7 @@ class CreateEDocument implements ShouldQueue
case "XInvoice-Basic": case "XInvoice-Basic":
//New implementation now the default 2025-02-04 - requires zugferd_version_two=false to disable //New implementation now the default 2025-02-04 - requires zugferd_version_two=false to disable
if(config('ninja.zugferd_version_two')){
$zugferd = (new ZugferdEDocument($this->document))->run(); $zugferd = (new ZugferdEDocument($this->document))->run();
}
else {
$zugferd = (new ZugferdEDokument($this->document))->run();
}
return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml(); return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml();
case "Facturae_3.2": case "Facturae_3.2":

View File

@ -37,6 +37,11 @@ class ZugferdEDocument extends AbstractService
private Client $client; private Client $client;
private InvoiceSum | InvoiceSumInclusive $calc; private InvoiceSum | InvoiceSumInclusive $calc;
private ?string $tax_code = null;
private ?string $exemption_reason_code = null;
/** /**
* __construct * __construct
* *
@ -93,44 +98,87 @@ class ZugferdEDocument extends AbstractService
private function setCustomSurcharges(): self private function setCustomSurcharges(): self
{ {
$item = $this->calc->getTaxMap()->first(); $item = $this->calc->getTaxMap()->first() ?: ['tax_rate' => 0, 'tax_id' => null];
$tax_code = $item['tax_id'] ? $this->getTaxType($item["tax_id"] ?? '2') : $this->tax_code;
if($this->document->custom_surcharge1 > 0){ if($this->document->custom_surcharge1 > 0){
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge1 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge1; $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge1 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge1;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"]); $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
} }
if($this->document->custom_surcharge2 > 0){ if($this->document->custom_surcharge2 > 0){
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge2 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge2; $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge2 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge2;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"]); $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
} }
if($this->document->custom_surcharge3 > 0){ if($this->document->custom_surcharge3 > 0){
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge3 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge3; $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge3 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge3;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"]); $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
} }
if($this->document->custom_surcharge4 > 0){ if($this->document->custom_surcharge4 > 0){
$surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge4 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge4; $surcharge = $this->document->uses_inclusive_taxes ? ($this->document->custom_surcharge4 / (1 + ($item["tax_rate"] / 100))) : $this->document->custom_surcharge4;
$this->xdocument->addDocumentAllowanceCharge($surcharge, true, $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"]); $this->xdocument->addDocumentAllowanceCharge($surcharge, true, $tax_code, "VAT", $item["tax_rate"]);
} }
return $this; return $this;
} }
/**
* setDocumentTaxes
*
* VATEX-EU-143 - Article 143 - Exemptions on importation
* VATEX-EU-146 - Article 146 - Exemptions on exportation
* VATEX-EU-148 - Article 148 - Exemptions for international transport
* VATEX-EU-151 - Article 151 - Exemptions for certain transactions
* VATEX-EU-169 - Article 169 - Right of deduction
* VATEX-EU-AE - Reverse charge - VAT to be paid by the recipient
* VATEX-EU-D - Triangulation rule - Intra-EU supply
* VATEX-EU-F - Free export item, tax not charged
* VATEX-EU-G - Export outside the EU
* VATEX-EU-IC - Intra-Community supply
* VATEX-EU-O - Outside scope of tax
* VATEX-EU-IC-SC - Intra-Community supply of services to customer in another member state
* VATEX-EU-AE-SC - Services to customer outside the EU
* VATEX-EU-NOT-TAX - Not subject to VAT
*
* @return self
*/
private function setDocumentTaxes(): self private function setDocumentTaxes(): self
{ {
if ($this->document->total_taxes == 0) { if ($this->document->total_taxes == 0) {
$base_amount = 0;
$tax_amount = 0;
$tax_rate = 0;
if($this->tax_code == ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE){ //reverse charge
$base_amount = $this->document->amount;
}
$this->xdocument->addDocumentTax( $this->xdocument->addDocumentTax(
ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX, $this->tax_code,
"VAT", "VAT",
0, $base_amount,
0, $tax_amount,
0, $tax_rate,
ctrans('texts.vat_not_registered'), null,
"VATNOTREG" $this->exemption_reason_code
); );
if ($this->calc->getTotalDiscount() > 0) {
$this->xdocument->addDocumentAllowanceCharge(
$this->calc->getTotalDiscount(),
false,
$this->tax_code,
"VAT",
0
);
}
return $this; return $this;
} }
@ -165,7 +213,7 @@ class ZugferdEDocument extends AbstractService
$this->xdocument->addDocumentAllowanceCharge( $this->xdocument->addDocumentAllowanceCharge(
round($this->calc->getTotalDiscount() * $ratio, 2), round($this->calc->getTotalDiscount() * $ratio, 2),
false, false,
$tax_type, $this->getTaxType($item["tax_id"] ?? '2'),
"VAT", "VAT",
$item["tax_rate"] $item["tax_rate"]
); );
@ -206,7 +254,27 @@ class ZugferdEDocument extends AbstractService
$this->calc = $this->document->calc(); $this->calc = $this->document->calc();
$br = new \App\DataMapper\Tax\BaseRule();
$eu_states = $br->eu_country_codes;
$item = $this->document->line_items[0];
if (!in_array($this->document->client->country->iso_3166_2, $eu_states)) {
$this->tax_code = ZugferdDutyTaxFeeCategories::FREE_EXPORT_ITEM_TAX_NOT_CHARGED;
$exemption_reason_code = "VATEX-EU-G";
} elseif ($this->client->is_tax_exempt || $item->tax_id == '5' || $item->tax_id == '8') {
$this->tax_code = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX;
$this->exemption_reason_code = "VATEX-EU-NOT-TAX";
} elseif ($item->tax_id == '9') { //reverse charge
$this->tax_code = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE;
$this->exemption_reason_code = "VATEX-EU-AE";
} elseif ($item->tax_id == '10') { //intra-community
$this->tax_code = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES;
$this->exemption_reason_code = "VATEX-EU-IC";
}
return $this; return $this;
} }
private function setDocumentSummation(): self private function setDocumentSummation(): self
@ -470,10 +538,7 @@ class ZugferdEDocument extends AbstractService
private function getDocumentLevelTaxRegistration(): string private function getDocumentLevelTaxRegistration(): string
{ {
$items = $this->document->line_items; return strlen($this->client->vat_number ?? '') > 1 ? "VA" : "FC";
$tax_id = $items[0]->tax_id ?? '1';
return $this->getTaxType($tax_id);
} }
private function getTaxType(string $tax_id): string private function getTaxType(string $tax_id): string
@ -513,7 +578,7 @@ class ZugferdEDocument extends AbstractService
if ((in_array($this->company->country()->iso_3166_2, $eu_states) && in_array($this->client->country->iso_3166_2, $eu_states)) && $this->company->country()->iso_3166_2 != $this->client->country->iso_3166_2) { if ((in_array($this->company->country()->iso_3166_2, $eu_states) && in_array($this->client->country->iso_3166_2, $eu_states)) && $this->company->country()->iso_3166_2 != $this->client->country->iso_3166_2) {
$tax_type = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; $tax_type = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES;
} elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) { } elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) {
$tax_type = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX; $tax_type = ZugferdDutyTaxFeeCategories::FREE_EXPORT_ITEM_TAX_NOT_CHARGED;
} elseif ($this->document->client->country->iso_3166_2 == "ES-CN") { } elseif ($this->document->client->country->iso_3166_2 == "ES-CN") {
$tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; $tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX;
} elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { } elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) {

View File

@ -198,6 +198,69 @@ class ZugferdTest extends TestCase
return compact('company', 'client', 'invoice'); return compact('company', 'client', 'invoice');
} }
public function testDeToNlReverseTax()
{
$scenario = [
'company_vat' => 'DE923356489',
'company_country' => 'DE',
'client_country' => 'NL',
'client_vat' => 'NL808436332B01',
'classification' => 'business',
'has_valid_vat' => true,
'over_threshold' => true,
'legal_entity_id' => 290868,
];
$data = $this->setupTestData($scenario);
$invoice = $data['invoice'];
$repo = new InvoiceRepository();
foreach($this->inclusive_scenarios as $scenario){
$invoice_data = json_decode($scenario, true);
$line_items = $invoice_data['line_items'];
foreach ($line_items as &$item) {
$item['tax_rate1'] = 0;
$item['tax_name1'] = '';
$item['tax_id'] = '9';
}
unset($item);
$invoice_data['line_items'] = array_values($line_items);
$invoice_data['uses_inclusive_taxes'] = false;
$invoice = $repo->save($invoice_data, $invoice);
$invoice = $invoice->calc()->getInvoice();
$xml = $invoice->service()->getEInvoice();
$validator = new \App\Services\EDocument\Standards\Validation\XsltDocumentValidator($xml);
$validator->setStyleSheets([$this->zug_16931]);
$validator->setXsd('/Services/EDocument/Standards/Validation/Zugferd/Schema/XSD/CrossIndustryInvoice_100pD22B.xsd');
$validator->validate();
if (count($validator->getErrors()) > 0) {
nlog($invoice->withoutRelations()->toArray());
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
}
public function testInclusiveScenarios() public function testInclusiveScenarios()
{ {
@ -232,7 +295,7 @@ class ZugferdTest extends TestCase
if (count($validator->getErrors()) > 0) { if (count($validator->getErrors()) > 0) {
nlog($invoice->toArray()); nlog($invoice->withoutRelations()->toArray());
nlog($xml); nlog($xml);
nlog($validator->getErrors()); nlog($validator->getErrors());
} }