diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 9a102b917c..be86ebf1bf 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -333,7 +333,7 @@ class BaseRule implements RuleInterface } public function defaultForeign(): self - { + {nlog("default foreign"); if ($this->invoice->client->is_tax_exempt) { $this->tax_rate1 = 0; diff --git a/app/Factory/InvoiceItemFactory.php b/app/Factory/InvoiceItemFactory.php index e0199de4ba..effadaf033 100644 --- a/app/Factory/InvoiceItemFactory.php +++ b/app/Factory/InvoiceItemFactory.php @@ -73,6 +73,7 @@ class InvoiceItemFactory $item->tax_name1 = 'GST'; $item->tax_rate1 = 10.00; $item->type_id = '1'; + $item->tax_id = '1'; $data[] = $item; } @@ -93,6 +94,7 @@ class InvoiceItemFactory $item->tax_name1 = 'GST'; $item->tax_rate1 = 10.00; $item->type_id = '2'; + $item->tax_id = '2'; $data[] = $item; @@ -127,7 +129,7 @@ class InvoiceItemFactory $item->tax_name1 = ''; $item->tax_rate1 = 0; $item->type_id = '1'; - + $item->tax_id = '1'; $data[] = $item; } diff --git a/app/Jobs/EDocument/CreateEDocument.php b/app/Jobs/EDocument/CreateEDocument.php index 48d4728b6f..5bd19e03ab 100644 --- a/app/Jobs/EDocument/CreateEDocument.php +++ b/app/Jobs/EDocument/CreateEDocument.php @@ -88,12 +88,7 @@ class CreateEDocument implements ShouldQueue case "XInvoice-Basic": //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(); - } - else { - $zugferd = (new ZugferdEDokument($this->document))->run(); - } + $zugferd = (new ZugferdEDocument($this->document))->run(); return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml(); case "Facturae_3.2": diff --git a/app/Services/EDocument/Standards/ZugferdEDocument.php b/app/Services/EDocument/Standards/ZugferdEDocument.php index c36aa9582e..1843342424 100644 --- a/app/Services/EDocument/Standards/ZugferdEDocument.php +++ b/app/Services/EDocument/Standards/ZugferdEDocument.php @@ -37,6 +37,11 @@ class ZugferdEDocument extends AbstractService private Client $client; private InvoiceSum | InvoiceSumInclusive $calc; + + private ?string $tax_code = null; + + private ?string $exemption_reason_code = null; + /** * __construct * @@ -93,44 +98,87 @@ class ZugferdEDocument extends AbstractService 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){ $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){ $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){ $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){ $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; } - + + /** + * 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 { 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( - ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX, + $this->tax_code, "VAT", - 0, - 0, - 0, - ctrans('texts.vat_not_registered'), - "VATNOTREG" + $base_amount, + $tax_amount, + $tax_rate, + null, + $this->exemption_reason_code ); + + if ($this->calc->getTotalDiscount() > 0) { + + $this->xdocument->addDocumentAllowanceCharge( + $this->calc->getTotalDiscount(), + false, + $this->tax_code, + "VAT", + 0 + ); + } + return $this; } @@ -165,7 +213,7 @@ class ZugferdEDocument extends AbstractService $this->xdocument->addDocumentAllowanceCharge( round($this->calc->getTotalDiscount() * $ratio, 2), false, - $tax_type, + $this->getTaxType($item["tax_id"] ?? '2'), "VAT", $item["tax_rate"] ); @@ -206,7 +254,27 @@ class ZugferdEDocument extends AbstractService $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; + } private function setDocumentSummation(): self @@ -470,10 +538,7 @@ class ZugferdEDocument extends AbstractService private function getDocumentLevelTaxRegistration(): string { - $items = $this->document->line_items; - $tax_id = $items[0]->tax_id ?? '1'; - return $this->getTaxType($tax_id); - + return strlen($this->client->vat_number ?? '') > 1 ? "VA" : "FC"; } 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) { $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)) { - $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") { $tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; } elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { diff --git a/tests/Integration/Einvoice/ZugferdTest.php b/tests/Integration/Einvoice/ZugferdTest.php index 3deb6f5010..8d9c6e6e80 100644 --- a/tests/Integration/Einvoice/ZugferdTest.php +++ b/tests/Integration/Einvoice/ZugferdTest.php @@ -198,6 +198,69 @@ class ZugferdTest extends TestCase 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() { @@ -232,7 +295,7 @@ class ZugferdTest extends TestCase if (count($validator->getErrors()) > 0) { - nlog($invoice->toArray()); + nlog($invoice->withoutRelations()->toArray()); nlog($xml); nlog($validator->getErrors()); }