Fixes for zugferd standard
This commit is contained in:
parent
168cd7add0
commit
bdb3676577
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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"])) {
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue