From 28637ae78c1c100483a5220b3118ea82f7fc4bca Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 21 Apr 2023 15:18:17 +1000 Subject: [PATCH 1/3] Refactor for e-invoices --- app/Jobs/Invoice/CreateXInvoice.php | 199 +-------------- .../Invoice/EInvoice/FacturaEInvoice.php | 10 +- .../Invoice/EInvoice/ZugferdEInvoice.php | 233 ++++++++++++++++++ 3 files changed, 245 insertions(+), 197 deletions(-) create mode 100644 app/Services/Invoice/EInvoice/ZugferdEInvoice.php diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 4ce8ff344cca..0db2523458c5 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -4,6 +4,8 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; use App\Models\Product; +use App\Services\Invoice\EInvoice\FacturaEInvoice; +use App\Services\Invoice\EInvoice\ZugferdEInvoice; use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; @@ -34,208 +36,23 @@ class CreateXInvoice implements ShouldQueue */ public function handle(): string { - $invoice = $this->invoice; - $company = $invoice->company; - $client = $invoice->client; - $profile = ""; - switch ($client->getSetting('e_invoice_type')) { + + switch ($this->invoice->client->getSetting('e_invoice_type')) { case "EN16931": - $profile = ZugferdProfiles::PROFILE_EN16931; - break; case "XInvoice_2_2": - $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_2; - break; case "XInvoice_2_1": - $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_1; - break; case "XInvoice_2_0": - $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2; - break; case "XInvoice_1_0": - $profile = ZugferdProfiles::PROFILE_XRECHNUNG; - break; case "XInvoice-Extended": - $profile = ZugferdProfiles::PROFILE_EXTENDED; - break; case "XInvoice-BasicWL": - $profile = ZugferdProfiles::PROFILE_BASICWL; - break; case "XInvoice-Basic": - $profile = ZugferdProfiles::PROFILE_BASIC; - break; + return (new ZugferdEInvoice($this->invoice, $this->alterPDF, $this->custom_pdf_path))->run(); + case "Facturae_3_2_2": + return (new FacturaEInvoice($this->invoice))->run(); default: - $profile = ZugferdProfiles::PROFILE_EN16931; + return (new ZugferdEInvoice($this->invoice, $this->alterPDF, $this->custom_pdf_path))->run(); break; } - $xrechnung = ZugferdDocumentBuilder::CreateNew($profile); - - $xrechnung - ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) - ->setDocumentSupplyChainEvent(date_create($invoice->date)) - ->setDocumentSeller($company->getSetting('name')) - ->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state")) - ->setDocumentSellerContact($invoice->user->first_name." ".$invoice->user->last_name, "", $invoice->user->phone, "", $invoice->user->email) - ->setDocumentBuyer($client->name, $client->number) - ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) - ->setDocumentBuyerContact($client->primary_contact()->first()->first_name . " " . $client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) - ->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state) - ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($invoice->date)->diff(date_create($invoice->due_date))->format("%d"), 'paydate' => $invoice->due_date])); - if (!empty($invoice->public_notes)) { - $xrechnung->addDocumentNote($invoice->public_notes); - } - if (!empty($invoice->po_number)) { - $xrechnung->setDocumentBuyerOrderReferencedDocument($invoice->po_number); - } - if (empty($client->routing_id)){ - $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); - } - else { - $xrechnung->setDocumentBuyerReference($client->routing_id); - } - $xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); - - if (str_contains($company->getSetting('vat_number'), "/")) { - $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); - } else { - $xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); - } - - $invoicing_data = $invoice->calc(); - $globaltax = null; - - //Create line items and calculate taxes - foreach ($invoice->line_items as $index => $item) { - $xrechnung->addNewPosition($index) - ->setDocumentPositionProductDetails($item->notes) - ->setDocumentPositionGrossPrice($item->gross_line_total) - ->setDocumentPositionNetPrice($item->line_total); - if (isset($item->task_id)) { - $xrechnung->setDocumentPositionQuantity($item->quantity, "HUR"); - } else { - $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); - } - $linenetamount = $item->line_total; - if ($item->discount > 0){ - if ($invoice->is_amount_discount){ - $linenetamount -= $item->discount; - } - else { - $linenetamount -= $linenetamount * ($item->discount / 100); - } - } - $xrechnung->setDocumentPositionLineSummation($linenetamount); - // According to european law, each line item can only have one tax rate - if (!(empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3))){ - if (!empty($item->tax_name1)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate1); - } elseif (!empty($item->tax_name2)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate2); - } elseif (!empty($item->tax_name3)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate3); - } else { - nlog("Can't add correct tax position"); - } - } else { - if (!empty($invoice->tax_name1)) { - $globaltax = 0; - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name1, $invoice), 'VAT', $invoice->tax_rate1); - } elseif (!empty($invoice->tax_name2)) { - $globaltax = 1; - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name2, $invoice), 'VAT', $invoice->tax_rate2); - } elseif (!empty($invoice->tax_name3)) { - $globaltax = 2; - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3, $invoice), 'VAT', $item->tax_rate3); - } else { - nlog("Can't add correct tax position"); - } - } - } - - - if ($invoice->isPartial()) { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), null, $invoice->partial); - } else { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), null, 0.0); - } - - foreach ($invoicing_data->getTaxMap() as $item) { - $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax($this->getTaxType("", $invoice), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); - // TODO: Add correct tax type within getTaxType - } - if (!empty($globaltax && isset($invoicing_data->getTotalTaxMap()[$globaltax]["name"]))){ - $tax = explode(" ", $invoicing_data->getTotalTaxMap()[$globaltax]["name"]); - $xrechnung->addDocumentTax($this->getTaxType("", $invoice), "VAT", $invoicing_data->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicing_data->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); - // TODO: Add correct tax type within getTaxType - } - - $disk = config('filesystems.default'); - if (!Storage::exists($client->e_invoice_filepath($invoice->invitations->first()))) { - Storage::makeDirectory($client->e_invoice_filepath($invoice->invitations->first())); - } - $xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); - // The validity can be checked using https://portal3.gefeg.com/invoice/validation - - if ($this->alterPDF) { - if ($this->custom_pdf_path != "") { - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $this->custom_pdf_path); - $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument($this->custom_pdf_path); - } else { - $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()) . $invoice->getFileName(); - $file = Storage::disk($disk)->exists($filepath_pdf); - if ($file) { - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, Storage::disk($disk)->path($filepath_pdf)); - $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf)); - } - } - } - - return $client->e_invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); - } - - private function getTaxType($name, Invoice $invoice): string - { - $taxtype = null; - switch ($name) { - case Product::PRODUCT_TYPE_SERVICE: - case Product::PRODUCT_TYPE_DIGITAL: - case Product::PRODUCT_TYPE_PHYSICAL: - case Product::PRODUCT_TYPE_SHIPPING: - case Product::PRODUCT_TYPE_REDUCED_TAX: - $taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE; - break; - case Product::PRODUCT_TYPE_EXEMPT: - $taxtype = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; - break; - case Product::PRODUCT_TYPE_ZERO_RATED: - $taxtype = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS; - break; - case Product::PRODUCT_TYPE_REVERSE_TAX: - $taxtype = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE; - break; - } - $eu_states = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "EL", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "CH"]; - if (empty($taxtype)){ - if (in_array($invoice->company->country()->iso_3166_2, $eu_states) && in_array($invoice->client->country->iso_3166_2, $eu_states)){ - $taxtype = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; - } - elseif (!in_array($invoice->client->country->iso_3166_2, $eu_states)){ - $taxtype = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX; - } - elseif ($invoice->client->country->iso_3166_2 == "ES-CN"){ - $taxtype = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; - } - elseif (in_array($invoice->client->country->iso_3166_2, ["ES-CE", "ES-ML"])){ - $taxtype = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA; - } - else { - nlog("Unkown tax case for xinvoice"); - $taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE; - } - } - return $taxtype; } } diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index c0d2da79cf49..ede0779891d8 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -11,10 +11,9 @@ namespace App\Services\Invoice\EInvoice; -use App\Models\Client; use App\Models\Invoice; -use josemmo\Facturae\Facturae; use App\Services\AbstractService; +use josemmo\Facturae\Facturae; use josemmo\Facturae\FacturaeItem; use josemmo\Facturae\FacturaeParty; @@ -135,7 +134,7 @@ class FacturaEInvoice extends AbstractService private function setPoNumber(): self { - if(strlen($this->invoice->po_number > 1)){ + if(strlen($this->invoice->po_number > 1)) { $this->fac->setReferences($this->invoice->po_number); } @@ -154,8 +153,7 @@ class FacturaEInvoice extends AbstractService private function buildItems(): self { - foreach($this->invoice->line_items as $item) - { + foreach($this->invoice->line_items as $item) { $this->fac->addItem(new FacturaeItem([ 'description' => $item->notes, 'quantity' => $item->quantity, @@ -299,4 +297,4 @@ class FacturaEInvoice extends AbstractService return $this; } -} \ No newline at end of file +} diff --git a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php new file mode 100644 index 000000000000..4bebf34032e8 --- /dev/null +++ b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php @@ -0,0 +1,233 @@ +invoice->company; + $client = $this->invoice->client; + $profile = $client->getSetting('e_invoice_type'); + + switch ($profile) { + case "EN16931": + $profile = ZugferdProfiles::PROFILE_EN16931; + break; + case "XInvoice_2_2": + $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_2; + break; + case "XInvoice_2_1": + $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2_1; + break; + case "XInvoice_2_0": + $profile = ZugferdProfiles::PROFILE_XRECHNUNG_2; + break; + case "XInvoice_1_0": + $profile = ZugferdProfiles::PROFILE_XRECHNUNG; + break; + case "XInvoice-Extended": + $profile = ZugferdProfiles::PROFILE_EXTENDED; + break; + case "XInvoice-BasicWL": + $profile = ZugferdProfiles::PROFILE_BASICWL; + break; + case "XInvoice-Basic": + $profile = ZugferdProfiles::PROFILE_BASIC; + break; + default: + $profile = ZugferdProfiles::PROFILE_EN16931; + break; + } + + + $xrechnung = ZugferdDocumentBuilder::CreateNew($profile); + + $xrechnung + ->setDocumentInformation($this->invoice->number, "380", date_create($this->invoice->date), $this->invoice->client->getCurrencyCode()) + ->setDocumentSupplyChainEvent(date_create($this->invoice->date)) + ->setDocumentSeller($company->getSetting('name')) + ->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state")) + ->setDocumentSellerContact($this->invoice->user->first_name." ".$this->invoice->user->last_name, "", $this->invoice->user->phone, "", $this->invoice->user->email) + ->setDocumentBuyer($client->name, $client->number) + ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) + ->setDocumentBuyerContact($client->primary_contact()->first()->first_name . " " . $client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) + ->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state) + ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->invoice->date)->diff(date_create($this->invoice->due_date))->format("%d"), 'paydate' => $this->invoice->due_date])); + if (!empty($this->invoice->public_notes)) { + $xrechnung->addDocumentNote($this->invoice->public_notes); + } + if (!empty($this->invoice->po_number)) { + $xrechnung->setDocumentBuyerOrderReferencedDocument($this->invoice->po_number); + } + if (empty($client->routing_id)) { + $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); + } else { + $xrechnung->setDocumentBuyerReference($client->routing_id); + } + $xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); + + if (str_contains($company->getSetting('vat_number'), "/")) { + $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); + } else { + $xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); + } + + $invoicing_data = $this->invoice->calc(); + $globaltax = null; + + //Create line items and calculate taxes + foreach ($this->invoice->line_items as $index => $item) { + $xrechnung->addNewPosition($index) + ->setDocumentPositionProductDetails($item->notes) + ->setDocumentPositionGrossPrice($item->gross_line_total) + ->setDocumentPositionNetPrice($item->line_total); + if (isset($item->task_id)) { + $xrechnung->setDocumentPositionQuantity($item->quantity, "HUR"); + } else { + $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); + } + $linenetamount = $item->line_total; + if ($item->discount > 0) { + if ($this->invoice->is_amount_discount) { + $linenetamount -= $item->discount; + } else { + $linenetamount -= $linenetamount * ($item->discount / 100); + } + } + $xrechnung->setDocumentPositionLineSummation($linenetamount); + // According to european law, each line item can only have one tax rate + if (!(empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3))) { + if (!empty($item->tax_name1)) { + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id), 'VAT', $item->tax_rate1); + } elseif (!empty($item->tax_name2)) { + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id), 'VAT', $item->tax_rate2); + } elseif (!empty($item->tax_name3)) { + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id), 'VAT', $item->tax_rate3); + } else { + nlog("Can't add correct tax position"); + } + } else { + if (!empty($this->invoice->tax_name1)) { + $globaltax = 0; + $xrechnung->addDocumentPositionTax($this->getTaxType($this->invoice->tax_name1), 'VAT', $this->invoice->tax_rate1); + } elseif (!empty($this->invoice->tax_name2)) { + $globaltax = 1; + $xrechnung->addDocumentPositionTax($this->getTaxType($this->invoice->tax_name2), 'VAT', $this->invoice->tax_rate2); + } elseif (!empty($this->invoice->tax_name3)) { + $globaltax = 2; + $xrechnung->addDocumentPositionTax($this->getTaxType($this->invoice->tax_name3), 'VAT', $item->tax_rate3); + } else { + nlog("Can't add correct tax position"); + } + } + } + + + if ($this->invoice->isPartial()) { + $xrechnung->setDocumentSummation($this->invoice->amount, $this->invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), null, $this->invoice->partial); + } else { + $xrechnung->setDocumentSummation($this->invoice->amount, $this->invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), null, 0.0); + } + + foreach ($invoicing_data->getTaxMap() as $item) { + $tax = explode(" ", $item["name"]); + $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); + // TODO: Add correct tax type within getTaxType + } + if (!empty($globaltax && isset($invoicing_data->getTotalTaxMap()[$globaltax]["name"]))) { + $tax = explode(" ", $invoicing_data->getTotalTaxMap()[$globaltax]["name"]); + $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $invoicing_data->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicing_data->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); + // TODO: Add correct tax type within getTaxType + } + + $disk = config('filesystems.default'); + if (!Storage::exists($client->e_invoice_filepath($this->invoice->invitations->first()))) { + Storage::makeDirectory($client->e_invoice_filepath($this->invoice->invitations->first())); + } + $xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml"))); + // The validity can be checked using https://portal3.gefeg.com/invoice/validation + + if ($this->alterPDF) { + if ($this->custom_pdf_path != "") { + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $this->custom_pdf_path); + $pdfBuilder->generateDocument(); + $pdfBuilder->saveDocument($this->custom_pdf_path); + } else { + $filepath_pdf = $client->invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName(); + $file = Storage::disk($disk)->exists($filepath_pdf); + if ($file) { + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, Storage::disk($disk)->path($filepath_pdf)); + $pdfBuilder->generateDocument(); + $pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf)); + } + } + } + + return $client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml"); + } + + private function getTaxType($name): string + { + $taxtype = null; + switch ($name) { + case Product::PRODUCT_TYPE_SERVICE: + case Product::PRODUCT_TYPE_DIGITAL: + case Product::PRODUCT_TYPE_PHYSICAL: + case Product::PRODUCT_TYPE_SHIPPING: + case Product::PRODUCT_TYPE_REDUCED_TAX: + $taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE; + break; + case Product::PRODUCT_TYPE_EXEMPT: + $taxtype = ZugferdDutyTaxFeeCategories::EXEMPT_FROM_TAX; + break; + case Product::PRODUCT_TYPE_ZERO_RATED: + $taxtype = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS; + break; + case Product::PRODUCT_TYPE_REVERSE_TAX: + $taxtype = ZugferdDutyTaxFeeCategories::VAT_REVERSE_CHARGE; + break; + } + $eu_states = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "EL", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "CH"]; + if (empty($taxtype)) { + if (in_array($this->invoice->company->country()->iso_3166_2, $eu_states) && in_array($this->invoice->client->country->iso_3166_2, $eu_states)) { + $taxtype = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; + } elseif (!in_array($this->invoice->client->country->iso_3166_2, $eu_states)) { + $taxtype = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX; + } elseif ($this->invoice->client->country->iso_3166_2 == "ES-CN") { + $taxtype = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; + } elseif (in_array($this->invoice->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { + $taxtype = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA; + } else { + nlog("Unkown tax case for xinvoice"); + $taxtype = ZugferdDutyTaxFeeCategories::STANDARD_RATE; + } + } + return $taxtype; + } + +} From f0487b2560f61b58688d69040b5720a2a018764f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 21 Apr 2023 15:44:11 +1000 Subject: [PATCH 2/3] Refactor for e-invoices --- app/Jobs/Entity/CreateEntityPdf.php | 4 +- ...{CreateXInvoice.php => CreateEInvoice.php} | 40 ++++++++++++------- app/Jobs/Invoice/ZipInvoices.php | 2 +- app/Services/Email/EmailDefaults.php | 4 +- .../Invoice/EInvoice/FacturaEInvoice.php | 20 ++++++++-- .../Invoice/EInvoice/ZugferdEInvoice.php | 11 ++++- app/Services/Invoice/GetInvoiceXInvoice.php | 4 +- app/Services/Invoice/InvoiceService.php | 6 +-- tests/Feature/EInvoice/FacturaeTest.php | 2 +- tests/Unit/EInvoiceTest.php | 8 ++-- 10 files changed, 65 insertions(+), 36 deletions(-) rename app/Jobs/Invoice/{CreateXInvoice.php => CreateEInvoice.php} (60%) diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index 631bf2d35ef5..6ee8b367f301 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -12,7 +12,7 @@ namespace App\Jobs\Entity; use App\Exceptions\FilePermissionsFailure; -use App\Jobs\Invoice\CreateXInvoice; +use App\Jobs\Invoice\CreateEInvoice; use App\Libraries\MultiDB; use App\Models\Credit; use App\Models\CreditInvitation; @@ -213,7 +213,7 @@ class CreateEntityPdf implements ShouldQueue } } if ($this->entity_string == "invoice" && $this->company->enable_e_invoice){ - (new CreateXInvoice($this->entity, true))->handle(); + (new CreateEInvoice($this->entity, true))->handle(); } $this->invitation = null; $this->entity = null; diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateEInvoice.php similarity index 60% rename from app/Jobs/Invoice/CreateXInvoice.php rename to app/Jobs/Invoice/CreateEInvoice.php index 0db2523458c5..e3cad18b3bf0 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateEInvoice.php @@ -2,23 +2,18 @@ namespace App\Jobs\Invoice; +use App\Utils\Ninja; use App\Models\Invoice; -use App\Models\Product; -use App\Services\Invoice\EInvoice\FacturaEInvoice; -use App\Services\Invoice\EInvoice\ZugferdEInvoice; -use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories; -use horstoeko\zugferd\ZugferdDocumentBuilder; -use horstoeko\zugferd\ZugferdDocumentPdfBuilder; -use horstoeko\zugferd\ZugferdProfiles; use Illuminate\Bus\Queueable; +use Illuminate\Support\Facades\App; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; -use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Storage; +use App\Services\Invoice\EInvoice\FacturaEInvoice; +use App\Services\Invoice\EInvoice\ZugferdEInvoice; - -class CreateXInvoice implements ShouldQueue +class CreateEInvoice implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -36,8 +31,20 @@ class CreateXInvoice implements ShouldQueue */ public function handle(): string { + /* Forget the singleton*/ + App::forgetInstance('translator'); - switch ($this->invoice->client->getSetting('e_invoice_type')) { + /* Init a new copy of the translator*/ + $t = app('translator'); + /* Set the locale*/ + App::setLocale($this->invoice->client->locale()); + + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->invoice->client->getMergedSettings())); + + $e_invoice_type = $this->invoice->client->getSetting('e_invoice_type'); + + switch ($e_invoice_type) { case "EN16931": case "XInvoice_2_2": case "XInvoice_2_1": @@ -47,12 +54,15 @@ class CreateXInvoice implements ShouldQueue case "XInvoice-BasicWL": case "XInvoice-Basic": return (new ZugferdEInvoice($this->invoice, $this->alterPDF, $this->custom_pdf_path))->run(); - case "Facturae_3_2_2": - return (new FacturaEInvoice($this->invoice))->run(); + case "Facturae_3.2": + case "Facturae_3.2.1": + case "Facturae_3.2.2": + return (new FacturaEInvoice($this->invoice, str_replace("Facturae_", "", $e_invoice_type)))->run(); default: return (new ZugferdEInvoice($this->invoice, $this->alterPDF, $this->custom_pdf_path))->run(); break; } + } } diff --git a/app/Jobs/Invoice/ZipInvoices.php b/app/Jobs/Invoice/ZipInvoices.php index 3eb9bd21823e..22476675c2f4 100644 --- a/app/Jobs/Invoice/ZipInvoices.php +++ b/app/Jobs/Invoice/ZipInvoices.php @@ -79,7 +79,7 @@ class ZipInvoices implements ShouldQueue $this->invoices->each(function ($invoice) { (new CreateEntityPdf($invoice->invitations()->first()))->handle(); if ($this->company->use_xinvoice){ - (new CreateXInvoice($invoice, false))->handle(); + (new CreateEInvoice($invoice, false))->handle(); } }); diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 13b8294bb146..0e13943b36c0 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -11,7 +11,7 @@ namespace App\Services\Email; -use App\Jobs\Invoice\CreateXInvoice; +use App\Jobs\Invoice\CreateEInvoice; use App\Services\Invoice\GetInvoiceXInvoice; use App\DataMapper\EmailTemplateDefaults; use App\Jobs\Entity\CreateRawPdf; @@ -301,7 +301,7 @@ class EmailDefaults if ($this->email->email_object->company->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) { $tempfile = tmpfile(); file_put_contents(stream_get_meta_data($tempfile)['uri'], $pdf); - $xinvoice_path = (new CreateXInvoice($this->email->email_object->entity, true, stream_get_meta_data($tempfile)['uri']))->handle(); + $xinvoice_path = (new CreateEInvoice($this->email->email_object->entity, true, stream_get_meta_data($tempfile)['uri']))->handle(); $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode(file_get_contents(stream_get_meta_data($tempfile)['uri'])), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]); $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode(file_get_contents($xinvoice_path)), 'name' => explode(".", $this->email->email_object->entity->getFileName('xml'))[0]."-xinvoice.xml"]]); } diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index ede0779891d8..7505956c74ab 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -12,10 +12,11 @@ namespace App\Services\Invoice\EInvoice; use App\Models\Invoice; -use App\Services\AbstractService; use josemmo\Facturae\Facturae; +use App\Services\AbstractService; use josemmo\Facturae\FacturaeItem; use josemmo\Facturae\FacturaeParty; +use Illuminate\Support\Facades\Storage; class FacturaEInvoice extends AbstractService { @@ -110,15 +111,16 @@ class FacturaEInvoice extends AbstractService // FacturaeCentre::ROLE_B2B_ISSUER Issuer in FACeB2B - public function __construct(public Invoice $invoice) + public function __construct(public Invoice $invoice, private mixed $profile) { } public function run() { + $this->calc = $this->invoice->calc(); - $this->fac = new Facturae(); + $this->fac = new Facturae($this->profile); $this->fac->setNumber('', $this->invoice->number); $this->fac->setIssueDate($this->invoice->date); $this->fac->setPrecision(Facturae::PRECISION_LINE); @@ -129,7 +131,17 @@ class FacturaEInvoice extends AbstractService ->setDiscount() ->setPoNumber(); - return $this->fac->export(); + + $disk = config('filesystems.default'); + + if (!Storage::disk($disk)->exists($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()))) { + Storage::makeDirectory($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first())); + } + + $this->fac->export(Storage::disk($disk)->path($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xsig"))); + + return $this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xsig"); + } private function setPoNumber(): self diff --git a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php index 4bebf34032e8..07e7c4cc25f8 100644 --- a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php +++ b/app/Services/Invoice/EInvoice/ZugferdEInvoice.php @@ -78,17 +78,21 @@ class ZugferdEInvoice extends AbstractService ->setDocumentBuyerContact($client->primary_contact()->first()->first_name . " " . $client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) ->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state) ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->invoice->date)->diff(date_create($this->invoice->due_date))->format("%d"), 'paydate' => $this->invoice->due_date])); - if (!empty($this->invoice->public_notes)) { + + if (!empty($this->invoice->public_notes)) { $xrechnung->addDocumentNote($this->invoice->public_notes); } + if (!empty($this->invoice->po_number)) { $xrechnung->setDocumentBuyerOrderReferencedDocument($this->invoice->po_number); } + if (empty($client->routing_id)) { $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); } else { $xrechnung->setDocumentBuyerReference($client->routing_id); } + $xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); if (str_contains($company->getSetting('vat_number'), "/")) { @@ -159,6 +163,7 @@ class ZugferdEInvoice extends AbstractService $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); // TODO: Add correct tax type within getTaxType } + if (!empty($globaltax && isset($invoicing_data->getTotalTaxMap()[$globaltax]["name"]))) { $tax = explode(" ", $invoicing_data->getTotalTaxMap()[$globaltax]["name"]); $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $invoicing_data->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicing_data->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); @@ -166,9 +171,11 @@ class ZugferdEInvoice extends AbstractService } $disk = config('filesystems.default'); - if (!Storage::exists($client->e_invoice_filepath($this->invoice->invitations->first()))) { + + if (!Storage::disk($disk)->exists($client->e_invoice_filepath($this->invoice->invitations->first()))) { Storage::makeDirectory($client->e_invoice_filepath($this->invoice->invitations->first())); } + $xrechnung->writeFile(Storage::disk($disk)->path($client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xml"))); // The validity can be checked using https://portal3.gefeg.com/invoice/validation diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php index 99881cabc0b5..9fcde4017a1e 100644 --- a/app/Services/Invoice/GetInvoiceXInvoice.php +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -11,7 +11,7 @@ namespace App\Services\Invoice; -use App\Jobs\Invoice\CreateXInvoice; +use App\Jobs\Invoice\CreateEInvoice; use App\Models\ClientContact; use App\Models\Invoice; use App\Services\AbstractService; @@ -43,7 +43,7 @@ class GetInvoiceXInvoice extends AbstractService $file = Storage::disk($disk)->exists($file_path); if (! $file) { - $file_path = (new CreateXInvoice($this->invoice, false))->handle(); + $file_path = (new CreateEInvoice($this->invoice, false))->handle(); } return $file_path; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index e7bf0d28375d..82693c3a634c 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -14,7 +14,7 @@ namespace App\Services\Invoice; use App\Events\Invoice\InvoiceWasArchived; use App\Jobs\Entity\CreateEntityPdf; use App\Jobs\Inventory\AdjustProductInventory; -use App\Jobs\Invoice\CreateXInvoice; +use App\Jobs\Invoice\CreateEInvoice; use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\CompanyGateway; use App\Models\Expense; @@ -451,7 +451,7 @@ class InvoiceService (new CreateEntityPdf($invitation))->handle(); if ($invitation instanceof InvoiceInvitation) { - (new CreateXInvoice($invitation->invoice, true))->handle(); + (new CreateEInvoice($invitation->invoice, true))->handle(); } }); @@ -462,7 +462,7 @@ class InvoiceService CreateEntityPdf::dispatch($invitation); if ($invitation instanceof InvoiceInvitation) { - CreateXInvoice::dispatch($invitation->invoice, true); + CreateEInvoice::dispatch($invitation->invoice, true); } }); } catch (\Exception $e) { diff --git a/tests/Feature/EInvoice/FacturaeTest.php b/tests/Feature/EInvoice/FacturaeTest.php index e0002ffba486..9f0f393e0398 100644 --- a/tests/Feature/EInvoice/FacturaeTest.php +++ b/tests/Feature/EInvoice/FacturaeTest.php @@ -40,7 +40,7 @@ class FacturaeTest extends TestCase public function testInvoiceGeneration() { - $f = new FacturaEInvoice($this->invoice); + $f = new FacturaEInvoice($this->invoice, "3.2.2"); $f->run(); $this->assertNotNull($f->run()); diff --git a/tests/Unit/EInvoiceTest.php b/tests/Unit/EInvoiceTest.php index 7f6a3ecb4bd9..6c31974bf3e9 100644 --- a/tests/Unit/EInvoiceTest.php +++ b/tests/Unit/EInvoiceTest.php @@ -12,7 +12,7 @@ use Tests\TestCase; use Tests\MockAccountData; use App\Jobs\Entity\CreateEntityPdf; -use App\Jobs\Invoice\CreateXInvoice; +use App\Jobs\Invoice\CreateEInvoice; use Illuminate\Support\Facades\Storage; use horstoeko\zugferd\ZugferdDocumentReader; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -42,7 +42,7 @@ class EInvoiceTest extends TestCase $this->company->e_invoice_type = "EN16931"; $this->invoice->client->routing_id = 'DE123456789'; $this->invoice->client->save(); - $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); + $xinvoice = (new CreateEInvoice($this->invoice, false))->handle(); $this->assertNotNull($xinvoice); $this->assertTrue(Storage::exists($xinvoice)); } @@ -56,7 +56,7 @@ class EInvoiceTest extends TestCase $this->invoice->client->routing_id = 'DE123456789'; $this->invoice->client->save(); - $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); + $xinvoice = (new CreateEInvoice($this->invoice, false))->handle(); nlog(Storage::path($xinvoice)); $document = ZugferdDocumentReader::readAndGuessFromFile(Storage::path($xinvoice)); $document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest); @@ -69,7 +69,7 @@ class EInvoiceTest extends TestCase public function checkEmbededPDFFile() { $pdf = (new CreateEntityPdf($this->invoice->invitations()->first()))->handle(); - (new CreateXInvoice($this->invoice, true, $pdf))->handle(); + (new CreateEInvoice($this->invoice, true, $pdf))->handle(); $document = ZugferdDocumentReader::readAndGuessFromFile($pdf); $document->getDocumentInformation($documentno, $documenttypecode, $documentdate, $documentcurrency, $taxcurrency, $taxname, $documentlangeuage, $rest); $this->assertEquals($this->invoice->number, $documentno); From 031fd819f55f85aa5cdd3fbb84ab629ae648237b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 21 Apr 2023 15:44:44 +1000 Subject: [PATCH 3/3] Add license header --- app/Jobs/Invoice/CreateEInvoice.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/Jobs/Invoice/CreateEInvoice.php b/app/Jobs/Invoice/CreateEInvoice.php index e3cad18b3bf0..1417b4ce73a1 100644 --- a/app/Jobs/Invoice/CreateEInvoice.php +++ b/app/Jobs/Invoice/CreateEInvoice.php @@ -1,4 +1,14 @@