From e023a8544e048ffca03436fa686135e0d4fe4d35 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sun, 12 Mar 2023 12:12:42 +0100 Subject: [PATCH 01/35] Inital version for XRechnung / ZugFerd --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index c85baeeb4327..56d84dfaa56d 100644 --- a/composer.json +++ b/composer.json @@ -90,6 +90,7 @@ "turbo124/beacon": "^1.4", "twilio/sdk": "^6.40", "webpatser/laravel-countries": "dev-master#75992ad", + "horstoeko/zugferd":"^1", "wepay/php-sdk": "^0.3" }, "require-dev": { From f3ed4abd9f0a0479f993b76679690c67d85e30e7 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sun, 12 Mar 2023 12:13:59 +0100 Subject: [PATCH 02/35] Inital version for XRechnung / ZugFerd --- app/Jobs/Invoice/CreateXRechnung.php | 80 ++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 app/Jobs/Invoice/CreateXRechnung.php diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXRechnung.php new file mode 100644 index 000000000000..8558b3380ef6 --- /dev/null +++ b/app/Jobs/Invoice/CreateXRechnung.php @@ -0,0 +1,80 @@ +invoice = $invoice; + } + + /** + * Execute the job. + * + * + * @return void + */ + public function handle() + { + $invoice = $this->invoice; + $company = $invoice->company; + $client = $invoice->client; + $xrechnung = ZugferdDocumentBuilder::CreateNew(ZugferdProfiles::PROFILE_EN16931); + + $xrechnung + ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) + ->addDocumentNote($invoice->public_notes) + ->setDocumentSupplyChainEvent(date_create($invoice->date)) + ->setDocumentSeller($company->name) + //->addDocumentSellerGlobalId("4000001123452", "0088") + //->addDocumentSellerTaxRegistration("FC", "201/113/40209") + ->addDocumentSellerTaxRegistration("VA", $company->vat_number) + ->setDocumentSellerAddress($company->address1, "", "", $company->postal_code, $company->city, $company->country->country->iso_3166_2) + ->setDocumentBuyer($client->name, $client->number) + ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2); + //->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018") + $taxamount_1 = $taxAmount_2 = $taxamount_3 = 0.0; + $netprice = 0.0; + + foreach ($invoice->line_items as $index => $item){ + $xrechnung->addNewPosition($index) + ->setDocumentPositionProductDetails($item->notes, "", "TB100A4", null, "0160", "4012345001235") + ->setDocumentPositionGrossPrice($item->gross_line_total) + ->setDocumentPositionNetPrice($item->line_total) + ->setDocumentPositionQuantity($item->quantity, "H87") + ->addDocumentPositionTax('S', 'VAT', 19); + $netprice += $item->line_total; + + } + if ($invoice->isPartial()){ + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, 0.0, 0.0, 473.00, $invoice->total_taxes, null, $invoice->partial);} + else { + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, 0.0, 0.0, 473.00, $invoice->total_taxes, null, 0.0); + } + if (strlen($invoice->tax_name1) > 1) { + $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate1); + } + if (strlen($invoice->tax_name2) > 1) { + $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate2); + } + if (strlen($invoice->tax_name3) > 1) { + $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate3); + }; + $xrechnung->writeFile(getcwd() . "/factur-x.xml"); + + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, "/tmp/original.pdf"); + $pdfBuilder->generateDocument(); + $pdfBuilder->saveDocument("/tmp/new.pdf"); + } +} From cd50941bc18829135cb23b637259e9df80d941a8 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sun, 12 Mar 2023 12:45:04 +0100 Subject: [PATCH 03/35] Added discounts and surcharges --- app/Jobs/Invoice/CreateXRechnung.php | 177 +++++++++++++++++++++++---- 1 file changed, 154 insertions(+), 23 deletions(-) diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXRechnung.php index 8558b3380ef6..f783dcafef36 100644 --- a/app/Jobs/Invoice/CreateXRechnung.php +++ b/app/Jobs/Invoice/CreateXRechnung.php @@ -3,6 +3,11 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; +use CleverIt\UBL\Invoice\Generator; +use CleverIt\UBL\Invoice\InvoiceLine; +use CleverIt\UBL\Invoice\Item; +use CleverIt\UBL\Invoice\LegalMonetaryTotal; +use CleverIt\UBL\Invoice\TaxTotal; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; @@ -44,37 +49,163 @@ class CreateXRechnung implements ShouldQueue ->setDocumentBuyer($client->name, $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2); //->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018") - $taxamount_1 = $taxAmount_2 = $taxamount_3 = 0.0; - $netprice = 0.0; - foreach ($invoice->line_items as $index => $item){ - $xrechnung->addNewPosition($index) - ->setDocumentPositionProductDetails($item->notes, "", "TB100A4", null, "0160", "4012345001235") - ->setDocumentPositionGrossPrice($item->gross_line_total) - ->setDocumentPositionNetPrice($item->line_total) - ->setDocumentPositionQuantity($item->quantity, "H87") - ->addDocumentPositionTax('S', 'VAT', 19); - $netprice += $item->line_total; + // Create line items and calculate taxes + $taxamount_1 = $taxAmount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; + $netprice = 0.0; + $chargetotalamount = $discount = 0.0; + $taxable = $this->getTaxable(); + foreach ($invoice->line_items as $index => $item){ + $xrechnung->addNewPosition($index) + ->setDocumentPositionProductDetails($item->notes, "", "TB100A4", null, "0160", "4012345001235") + ->setDocumentPositionGrossPrice($item->gross_line_total) + ->setDocumentPositionNetPrice($item->line_total) + ->setDocumentPositionQuantity($item->quantity, "H87") + ->addDocumentPositionTax('S', 'VAT', 19); + $netprice += $this->getItemTaxable($item, $taxable); + + // TODO: add tax rate + + if ($item->discount > 0){ + if ($invoice->is_amount_discount){ + $discount += $item->discount; + } + else { + $discount += $item->line_total * ($item->discount / 100); + } + } + } + + // Calculate global surcharges + if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) { + $chargetotalamount += $this->invoice->custom_surcharge1; + } + + if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) { + $chargetotalamount += $this->invoice->custom_surcharge2; + } + + if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) { + $chargetotalamount += $this->invoice->custom_surcharge3; + } + + if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) { + $chargetotalamount += $this->invoice->custom_surcharge4; + } + + // Calculate global discounts + if ($invoice->disount > 0){ + if ($invoice->is_amount_discount){ + $discount += $invoice->discount; } - if ($invoice->isPartial()){ - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, 0.0, 0.0, 473.00, $invoice->total_taxes, null, $invoice->partial);} else { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, 0.0, 0.0, 473.00, $invoice->total_taxes, null, 0.0); + $discount += $invoice->amount * ($invoice->discount / 100); } - if (strlen($invoice->tax_name1) > 1) { - $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate1); - } - if (strlen($invoice->tax_name2) > 1) { - $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate2); - } - if (strlen($invoice->tax_name3) > 1) { - $xrechnung->addDocumentTax("S", "VAT", 275.0, 19.25, $invoice->tax_rate3); - }; - $xrechnung->writeFile(getcwd() . "/factur-x.xml"); + } + + if ($invoice->isPartial()){ + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, $invoice->partial);} + else { + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, 0.0); + } + if (strlen($invoice->tax_name1) > 1) { + $xrechnung->addDocumentTax("S", "VAT", $taxnet_1, $taxamount_1, $invoice->tax_rate1); + } + if (strlen($invoice->tax_name2) > 1) { + $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxAmount_2, $invoice->tax_rate2); + } + if (strlen($invoice->tax_name3) > 1) { + $xrechnung->addDocumentTax("S", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); + }; + $xrechnung->writeFile(getcwd() . "/factur-x.xml"); + + // TODO: Inject XML into PDF $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, "/tmp/original.pdf"); $pdfBuilder->generateDocument(); $pdfBuilder->saveDocument("/tmp/new.pdf"); } + private function getItemTaxable($item, $invoice_total): float + { + $total = $item->quantity * $item->cost; + + if ($this->invoice->discount != 0) { + if ($this->invoice->is_amount_discount) { + if ($invoice_total + $this->invoice->discount != 0) { + $total -= $invoice_total ? ($total / ($invoice_total + $this->invoice->discount) * $this->invoice->discount) : 0; + } + } else { + $total *= (100 - $this->invoice->discount) / 100; + } + } + + if ($item->discount != 0) { + if ($this->invoice->is_amount_discount) { + $total -= $item->discount; + } else { + $total -= $total * $item->discount / 100; + } + } + + return round($total, 2); + } + + /** + * @return float + */ + private function getTaxable(): float + { + $total = 0.0; + + foreach ($this->invoice->line_items as $item) { + $line_total = $item->quantity * $item->cost; + + if ($item->discount != 0) { + if ($this->invoice->is_amount_discount) { + $line_total -= $item->discount; + } else { + $line_total -= $line_total * $item->discount / 100; + } + } + + $total += $line_total; + } + + if ($this->invoice->discount > 0) { + if ($this->invoice->is_amount_discount) { + $total -= $this->invoice->discount; + } else { + $total *= (100 - $this->invoice->discount) / 100; + $total = round($total, 2); + } + } + + if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) { + $total += $this->invoice->custom_surcharge1; + } + + if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) { + $total += $this->invoice->custom_surcharge2; + } + + if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) { + $total += $this->invoice->custom_surcharge3; + } + + if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) { + $total += $this->invoice->custom_surcharge4; + } + + return $total; + } + + public function taxAmount($taxable, $rate): float + { + if ($this->invoice->uses_inclusive_taxes) { + return round($taxable - ($taxable / (1 + ($rate / 100))), 2); + } else { + return round($taxable * ($rate / 100), 2); + } + } } From c11ff31cc3a88e7b1785f895da4f07ead72b4a64 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sun, 12 Mar 2023 12:46:10 +0100 Subject: [PATCH 04/35] Minor fixes --- app/Jobs/Invoice/CreateXRechnung.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXRechnung.php index f783dcafef36..6a78fedbd413 100644 --- a/app/Jobs/Invoice/CreateXRechnung.php +++ b/app/Jobs/Invoice/CreateXRechnung.php @@ -11,13 +11,12 @@ use CleverIt\UBL\Invoice\TaxTotal; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; +use Illuminate\Contracts\Queue\ShouldQueue; class CreateXRechnung implements ShouldQueue { - use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - - public $invoice; + public Invoice $invoice; public function __construct(Invoice $invoice) { @@ -30,7 +29,7 @@ class CreateXRechnung implements ShouldQueue * * @return void */ - public function handle() + public function handle(): void { $invoice = $this->invoice; $company = $invoice->company; From 454182f49c71edaa7ab0cba78d706b88d9b6cec6 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Sun, 12 Mar 2023 12:58:48 +0100 Subject: [PATCH 05/35] Improve implementation --- app/Jobs/Invoice/CreateXRechnung.php | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXRechnung.php index 6a78fedbd413..faddda18a3ab 100644 --- a/app/Jobs/Invoice/CreateXRechnung.php +++ b/app/Jobs/Invoice/CreateXRechnung.php @@ -11,11 +11,17 @@ use CleverIt\UBL\Invoice\TaxTotal; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; +use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; class CreateXRechnung implements ShouldQueue { + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + public Invoice $invoice; public function __construct(Invoice $invoice) @@ -41,14 +47,17 @@ class CreateXRechnung implements ShouldQueue ->addDocumentNote($invoice->public_notes) ->setDocumentSupplyChainEvent(date_create($invoice->date)) ->setDocumentSeller($company->name) - //->addDocumentSellerGlobalId("4000001123452", "0088") - //->addDocumentSellerTaxRegistration("FC", "201/113/40209") - ->addDocumentSellerTaxRegistration("VA", $company->vat_number) ->setDocumentSellerAddress($company->address1, "", "", $company->postal_code, $company->city, $company->country->country->iso_3166_2) ->setDocumentBuyer($client->name, $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2); //->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018") + if (str_contains($company->vat_number, "/")){ + $xrechnung->addDocumentSellerTaxRegistration("FC", $company->vat_number); + } + else { + $xrechnung->addDocumentSellerTaxRegistration("VA", $company->vat_number); + } // Create line items and calculate taxes $taxamount_1 = $taxAmount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; $netprice = 0.0; @@ -57,14 +66,18 @@ class CreateXRechnung implements ShouldQueue foreach ($invoice->line_items as $index => $item){ $xrechnung->addNewPosition($index) - ->setDocumentPositionProductDetails($item->notes, "", "TB100A4", null, "0160", "4012345001235") + ->setDocumentPositionProductDetails($item->notes) ->setDocumentPositionGrossPrice($item->gross_line_total) - ->setDocumentPositionNetPrice($item->line_total) - ->setDocumentPositionQuantity($item->quantity, "H87") - ->addDocumentPositionTax('S', 'VAT', 19); + ->setDocumentPositionNetPrice($item->line_total); + if (isset($item->task_id)){ + $xrechnung->setDocumentPositionQuantity($item->quantity, "HUR"); + } + else{ + $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); + } $netprice += $this->getItemTaxable($item, $taxable); - // TODO: add tax rate + $xrechnung->addDocumentPositionTax('S', 'VAT', 19); if ($item->discount > 0){ if ($invoice->is_amount_discount){ From 060cc2d6a0e344ff61ab9d86966dd04727410569 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 13 Mar 2023 08:01:17 +0100 Subject: [PATCH 06/35] Add support for "Leitweg-ID" and po-number --- app/Jobs/Invoice/CreateXRechnung.php | 7 +++-- app/Models/Client.php | 3 ++ ...156872_add_letiweg_id_to_clients_table.php | 31 +++++++++++++++++++ lang/en/texts.php | 1 + 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXRechnung.php index faddda18a3ab..cfe5fec9014d 100644 --- a/app/Jobs/Invoice/CreateXRechnung.php +++ b/app/Jobs/Invoice/CreateXRechnung.php @@ -49,8 +49,11 @@ class CreateXRechnung implements ShouldQueue ->setDocumentSeller($company->name) ->setDocumentSellerAddress($company->address1, "", "", $company->postal_code, $company->city, $company->country->country->iso_3166_2) ->setDocumentBuyer($client->name, $client->number) - ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2); - //->addDocumentPaymentTerm("Zahlbar innerhalb 30 Tagen netto bis 04.04.2018, 3% Skonto innerhalb 10 Tagen bis 15.03.2018") + ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2) + ->setDocumentBuyerReference($client->leitweg_id) + ->setDocumentBuyerContact($client->primary_contact->first_name." ".$client->primary_contact->last_name, "", $client->primary_contact->phone, "", $client->primary_contact->email) + ->setDocumentBuyerOrderReferencedDocument($invoice->po_number) + ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($invoice->date)->diff(date_create($invoice->due_date))->format("%d"), 'paydate' => $invoice->due_date])); if (str_contains($company->vat_number, "/")){ $xrechnung->addDocumentSellerTaxRegistration("FC", $company->vat_number); diff --git a/app/Models/Client.php b/app/Models/Client.php index 2259a449adfb..25b553f25f4c 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -41,6 +41,7 @@ use Laracasts\Presenter\PresentableTrait; * @property string|null $client_hash * @property string|null $logo * @property string|null $phone + * @property string|null leitweg_id * @property string $balance * @property string $paid_to_date * @property string $credit_balance @@ -220,6 +221,7 @@ class Client extends BaseModel implements HasLocalePreference 'public_notes', 'phone', 'number', + 'leitweg_id', ]; protected $with = [ @@ -268,6 +270,7 @@ class Client extends BaseModel implements HasLocalePreference 'id_number', 'public_notes', 'phone', + 'leitweg_id', ]; // public function scopeExclude($query) diff --git a/database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php b/database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php new file mode 100644 index 000000000000..c176897dd993 --- /dev/null +++ b/database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php @@ -0,0 +1,31 @@ +string('leitweg_idf')->default(null); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index 8d6ff548d554..69d2394c31c6 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5020,6 +5020,7 @@ $LANG = array( 'white_label_body' => 'Thank you for purchasing a white label license.

Your license key is:

:license_key', 'payment_type_Klarna' => 'Klarna', 'payment_type_Interac E Transfer' => 'Interac E Transfer', + 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', ); From d4bc9de4723d08f64b003a376b91068e30e032cf Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 13 Mar 2023 08:07:54 +0100 Subject: [PATCH 07/35] Renamed class to respect english convention Added support for saving file --- .../{CreateXRechnung.php => CreateXInvoice.php} | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) rename app/Jobs/Invoice/{CreateXRechnung.php => CreateXInvoice.php} (95%) diff --git a/app/Jobs/Invoice/CreateXRechnung.php b/app/Jobs/Invoice/CreateXInvoice.php similarity index 95% rename from app/Jobs/Invoice/CreateXRechnung.php rename to app/Jobs/Invoice/CreateXInvoice.php index cfe5fec9014d..c012c4cc52e2 100644 --- a/app/Jobs/Invoice/CreateXRechnung.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -3,11 +3,6 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; -use CleverIt\UBL\Invoice\Generator; -use CleverIt\UBL\Invoice\InvoiceLine; -use CleverIt\UBL\Invoice\Item; -use CleverIt\UBL\Invoice\LegalMonetaryTotal; -use CleverIt\UBL\Invoice\TaxTotal; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; @@ -18,7 +13,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -class CreateXRechnung implements ShouldQueue +class CreateXInvoice implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -133,13 +128,13 @@ class CreateXRechnung implements ShouldQueue } if (strlen($invoice->tax_name3) > 1) { $xrechnung->addDocumentTax("S", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); - }; - $xrechnung->writeFile(getcwd() . "/factur-x.xml"); + } + $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); // TODO: Inject XML into PDF - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, "/tmp/original.pdf"); + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $client->invoice_filepath($invoice->invitations->first())); $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument("/tmp/new.pdf"); + $pdfBuilder->saveDocument($client->invoice_filepath($invoice->invitations->first())); } private function getItemTaxable($item, $invoice_total): float { From a765153642eb097b3a9c3de50980db86ae2a9b60 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 13 Mar 2023 08:28:46 +0100 Subject: [PATCH 08/35] Create XInvoice automatically, when enabled --- app/Jobs/Invoice/CreateXInvoice.php | 21 +++++--- app/Jobs/Invoice/ZipInvoices.php | 8 ++- app/Services/Invoice/GetInvoiceXInvoice.php | 56 +++++++++++++++++++++ app/Services/Invoice/InvoiceService.php | 31 +++++++++++- 4 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 app/Services/Invoice/GetInvoiceXInvoice.php diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index c012c4cc52e2..1970aad1874f 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -11,6 +11,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Storage; class CreateXInvoice implements ShouldQueue @@ -28,9 +29,9 @@ class CreateXInvoice implements ShouldQueue * Execute the job. * * - * @return void + * @return string */ - public function handle(): void + public function handle(): string { $invoice = $this->invoice; $company = $invoice->company; @@ -127,14 +128,20 @@ class CreateXInvoice implements ShouldQueue $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxAmount_2, $invoice->tax_rate2); } if (strlen($invoice->tax_name3) > 1) { - $xrechnung->addDocumentTax("S", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); + $xrechnung->addDocumentTax("CS", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); - // TODO: Inject XML into PDF - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $client->invoice_filepath($invoice->invitations->first())); - $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument($client->invoice_filepath($invoice->invitations->first())); + $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()); + $disk = config('filesystems.default'); + + $file = Storage::disk($disk)->exists($filepath_pdf); + if ($file) { + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $filepath_pdf); + $pdfBuilder->generateDocument(); + $pdfBuilder->saveDocument($client->invoice_filepath($invoice->invitations->first())); + } + return explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"; } private function getItemTaxable($item, $invoice_total): float { diff --git a/app/Jobs/Invoice/ZipInvoices.php b/app/Jobs/Invoice/ZipInvoices.php index 9a0d4752c3a8..4d6dca8fe63a 100644 --- a/app/Jobs/Invoice/ZipInvoices.php +++ b/app/Jobs/Invoice/ZipInvoices.php @@ -78,13 +78,19 @@ class ZipInvoices implements ShouldQueue $this->invoices->each(function ($invoice) { (new CreateEntityPdf($invoice->invitations()->first()))->handle(); + if ($this->company->getSetting("create_xinvoice")){ + (new CreateXInvoice($invoice))->handle(); + } }); try { foreach ($this->invoices as $invoice) { $file = $invoice->service()->getInvoicePdf(); + $xinvoice = $invoice->service()->getXInvoice(); $zip_file_name = basename($file); - $zipFile->addFromString($zip_file_name, Storage::get($file)); + $xinvoice_zip_file_name = basename($xinvoice); + $zipFile->addFromString($zip_file_name, Storage::get($file)) + ->addDir($xinvoice_zip_file_name, Storage::get($xinvoice)); //$download_file = file_get_contents($invoice->pdf_file_path($invitation, 'url', true)); //$zipFile->addFromString(basename($invoice->pdf_file_path($invitation)), $download_file); diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php new file mode 100644 index 000000000000..a4f2e0e37e9b --- /dev/null +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -0,0 +1,56 @@ +invoice = $invoice; + + $this->contact = $contact; + } + + public function run() + { + if (! $this->contact) { + $this->contact = $this->invoice->client->primary_contact()->first() ?: $this->invoice->client->contacts()->first(); + } + + $invitation = $this->invoice->invitations->where('client_contact_id', $this->contact->id)->first(); + + if (! $invitation) { + $invitation = $this->invoice->invitations->first(); + } + + $path = $this->invoice->client->invoice_filepath($invitation); + + $file_path = $path.$this->invoice->numberFormatter().'-xinvoice.xml'; + + // $disk = 'public'; + $disk = config('filesystems.default'); + + $file = Storage::disk($disk)->exists($file_path); + + if (! $file) { + $file_path = (new CreateXInvoice($this->invoice))->handle(); + } + + return $file_path; + } +} diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 6c4fe4882003..68cb3101ef32 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -14,6 +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\Libraries\Currency\Conversion\CurrencyApi; use App\Models\CompanyGateway; use App\Models\Expense; @@ -184,6 +185,11 @@ class InvoiceService return (new GenerateDeliveryNote($invoice, $contact))->run(); } + public function getXInvoice($contact = null) + { + return (new GetInvoiceXInvoice($this->invoice))->run(); + } + public function sendEmail($contact = null) { $send_email = new SendEmail($this->invoice, null, $contact); @@ -293,7 +299,7 @@ class InvoiceService } elseif ($this->invoice->balance < 0 || $this->invoice->balance > 0) { $this->invoice->status_id = Invoice::STATUS_SENT; } - + return $this; } @@ -351,6 +357,27 @@ class InvoiceService return $this; } + public function deleteXInvoice() + { + $this->invoice->load('invitations'); + + $this->invoice->invitations->each(function ($invitation) { + try { + if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml')) { + Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml'); + } + + if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml')) { + Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml'); + } + } catch (\Exception $e) { + nlog($e->getMessage()); + } + }); + + return $this; + } + public function removeUnpaidGatewayFees() { $balance = $this->invoice->balance; @@ -421,6 +448,7 @@ class InvoiceService if ($force) { $this->invoice->invitations->each(function ($invitation) { (new CreateEntityPdf($invitation))->handle(); + // Add XInvoice }); return $this; @@ -428,6 +456,7 @@ class InvoiceService $this->invoice->invitations->each(function ($invitation) { CreateEntityPdf::dispatch($invitation); + // Add XInvoice }); } catch (\Exception $e) { nlog('failed creating invoices in Touch PDF'); From dd49768aef44434c93b847e807d2eeb2cfa08cfc Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 13 Mar 2023 08:50:37 +0100 Subject: [PATCH 09/35] Fixes for XInvoice and differniate between Profiles --- app/Console/Commands/SendRemindersCron.php | 3 ++ app/Jobs/Invoice/CreateXInvoice.php | 29 ++++++++++++++++++- app/Jobs/Invoice/ZipInvoices.php | 2 +- app/Models/Company.php | 4 +++ ...56872_add_leitweg_id_to_clients_table.php} | 4 +++ 5 files changed, 40 insertions(+), 2 deletions(-) rename database/migrations/{2023_03_13_156872_add_letiweg_id_to_clients_table.php => 2023_03_13_156872_add_leitweg_id_to_clients_table.php} (72%) diff --git a/app/Console/Commands/SendRemindersCron.php b/app/Console/Commands/SendRemindersCron.php index 7319ec30f727..68da45d79e21 100644 --- a/app/Console/Commands/SendRemindersCron.php +++ b/app/Console/Commands/SendRemindersCron.php @@ -175,6 +175,9 @@ class SendRemindersCron extends Command $invoice->calc()->getInvoice()->save(); $invoice->fresh(); $invoice->service()->deletePdf()->save(); + if ($invoice->company->use_xinvoice){ + $invoice->service()->deleteXInvoice()->save(); + } /* Refresh the client here to ensure the balance is fresh */ $client = $invoice->client; diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 1970aad1874f..69c9e49cb709 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -36,7 +36,34 @@ class CreateXInvoice implements ShouldQueue $invoice = $this->invoice; $company = $invoice->company; $client = $invoice->client; - $xrechnung = ZugferdDocumentBuilder::CreateNew(ZugferdProfiles::PROFILE_EN16931); + $profile = ""; + switch ($company->xinvoice_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; + } + $xrechnung = ZugferdDocumentBuilder::CreateNew($profile); $xrechnung ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) diff --git a/app/Jobs/Invoice/ZipInvoices.php b/app/Jobs/Invoice/ZipInvoices.php index 4d6dca8fe63a..064d42f7f496 100644 --- a/app/Jobs/Invoice/ZipInvoices.php +++ b/app/Jobs/Invoice/ZipInvoices.php @@ -78,7 +78,7 @@ class ZipInvoices implements ShouldQueue $this->invoices->each(function ($invoice) { (new CreateEntityPdf($invoice->invitations()->first()))->handle(); - if ($this->company->getSetting("create_xinvoice")){ + if ($this->company->use_xinvoice){ (new CreateXInvoice($invoice))->handle(); } }); diff --git a/app/Models/Company.php b/app/Models/Company.php index 49531af286ce..c31b894c21ff 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -97,6 +97,8 @@ use Laracasts\Presenter\PresentableTrait; * @property int $stock_notification * @property string|null $matomo_url * @property int|null $matomo_id + * @property bool $use_xinvoice + * @property string $xinvoice_type * @property int $enabled_expense_tax_rates * @property int $invoice_task_project * @property int $report_include_deleted @@ -354,6 +356,8 @@ class Company extends BaseModel 'google_analytics_key', 'matomo_url', 'matomo_id', + 'use_xinvoice', + 'xinvoice_type', 'client_can_register', 'enable_shop_api', 'invoice_task_timelog', diff --git a/database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php b/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php similarity index 72% rename from database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php rename to database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php index c176897dd993..4bc691615a47 100644 --- a/database/migrations/2023_03_13_156872_add_letiweg_id_to_clients_table.php +++ b/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php @@ -17,6 +17,10 @@ return new class extends Migration Schema::table('clients', function (Blueprint $table) { $table->string('leitweg_idf')->default(null); }); + Schema::table('companies', function (Blueprint $table) { + $table->boolean('use_xinvoice')->default(false); + $table->string('xinvoice_type')->default("EN16931"); + }); } /** From b1be93828ab5acca1da567e06a8744ca5e65f844 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 14 Mar 2023 21:26:08 +0100 Subject: [PATCH 10/35] Implement different invoice taxes --- app/Jobs/Invoice/CreateXInvoice.php | 55 ++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 69c9e49cb709..7e1e21aaa772 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -85,7 +85,7 @@ class CreateXInvoice implements ShouldQueue $xrechnung->addDocumentSellerTaxRegistration("VA", $company->vat_number); } // Create line items and calculate taxes - $taxamount_1 = $taxAmount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; + $taxamount_1 = $taxamount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; $netprice = 0.0; $chargetotalamount = $discount = 0.0; $taxable = $this->getTaxable(); @@ -102,17 +102,56 @@ class CreateXInvoice implements ShouldQueue $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); } $netprice += $this->getItemTaxable($item, $taxable); - - $xrechnung->addDocumentPositionTax('S', 'VAT', 19); - + $discountamount = 0.0; if ($item->discount > 0){ if ($invoice->is_amount_discount){ + $discountamount = $item->discount; $discount += $item->discount; } else { + $discountamount = $item->line_total * ($item->discount / 100); $discount += $item->line_total * ($item->discount / 100); } } + + // According to european law, each artical can only have one tax percentage + if ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ + if ($invoice->tax_name1 != null && $invoice->tax_name2 == null && $invoice->tax_name3 == null){ + $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate1); + $taxnet_1 += $item->line_total - $discountamount; + $taxamount_1 += $item->tax_amount; + } + elseif ($invoice->tax_name1 == null && $invoice->tax_name2 != null && $invoice->tax_name3 == null){ + $taxnet_2 += $item->line_total - $discountamount; + $taxamount_2 += $item->tax_amount; + $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate2); + } + elseif ($invoice->tax_name1 == null && $invoice->tax_name2 == null && $invoice->tax_name3 != null){ + $taxnet_3 += $item->line_total - $discountamount; + $taxamount_3 += $item->tax_amount; + $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate3); + } + else{ + nlog("Can't add correct tax position"); + } + } + else { + if ($item->tax_name1 != "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ + $taxnet_1 += $item->line_total - $discountamount; + $taxamount_1 += $item->tax_amount; + $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate1); + } + elseif ($item->tax_name1 == "" && $item->tax_name2 != "" && $item->tax_name3 == ""){ + $taxnet_2 += $item->line_total - $discountamount; + $taxamount_2 += $item->tax_amount; + $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate2); + } + elseif ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 != ""){ + $taxnet_3 += $item->line_total - $discountamount; + $taxamount_3 += $item->tax_amount; + $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate3); + } + } } // Calculate global surcharges @@ -148,13 +187,13 @@ class CreateXInvoice implements ShouldQueue else { $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, 0.0); } - if (strlen($invoice->tax_name1) > 1) { + if ($taxnet_1 > 0){ $xrechnung->addDocumentTax("S", "VAT", $taxnet_1, $taxamount_1, $invoice->tax_rate1); } - if (strlen($invoice->tax_name2) > 1) { - $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxAmount_2, $invoice->tax_rate2); + if ($taxnet_2 > 0) { + $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxamount_2, $invoice->tax_rate2); } - if (strlen($invoice->tax_name3) > 1) { + if ($taxnet_3 > 0) { $xrechnung->addDocumentTax("CS", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); From d5bb25fb35ce3568ec6a2c723c3a7d8620bced06 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 14 Mar 2023 21:26:28 +0100 Subject: [PATCH 11/35] Minor fix --- app/Jobs/Invoice/CreateXInvoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 7e1e21aaa772..b9a0f359808c 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -194,7 +194,7 @@ class CreateXInvoice implements ShouldQueue $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxamount_2, $invoice->tax_rate2); } if ($taxnet_3 > 0) { - $xrechnung->addDocumentTax("CS", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); + $xrechnung->addDocumentTax("S", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); From 92f267837012744200fa6ff0777b030073a0d937 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 14 Mar 2023 21:39:17 +0100 Subject: [PATCH 12/35] Add different types of taxes in the european union --- app/Jobs/Invoice/CreateXInvoice.php | 109 +++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index b9a0f359808c..a65e8272d642 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -12,6 +12,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Storage; +use function Symfony\Component\String\b; class CreateXInvoice implements ShouldQueue @@ -85,6 +86,96 @@ class CreateXInvoice implements ShouldQueue $xrechnung->addDocumentSellerTaxRegistration("VA", $company->vat_number); } // Create line items and calculate taxes + $taxtype1 = ""; + switch ($company->tax_type1){ + case "Sales Tax": + $taxtype1 = "S"; + break; + case "ZeroRate": + $taxtype1 = "Z"; + break; + case "Tax Exempt": + $taxtype1 = "E"; + break; + case "Reversal of tax liabilty": + $taxtype1 = "AE"; + break; + case "intra-community delivery": + $taxtype1 = "K"; + break; + case "Out of EU": + $taxtype1 = "G"; + break; + case "Outside the tax scope": + $taxtype1 = "O"; + break; + case "Canary Islands": + $taxtype1 = "L"; + break; + case "Ceuta / Melila": + $taxtype1 = "M"; + break; + } + $taxtype2 = ""; + switch ($company->tax_type2){ + case "Sales Tax": + $taxtype2 = "S"; + break; + case "ZeroRate": + $taxtype2 = "Z"; + break; + case "Tax Exempt": + $taxtype2 = "E"; + break; + case "Reversal of tax liabilty": + $taxtype2 = "AE"; + break; + case "intra-community delivery": + $taxtype2 = "K"; + break; + case "Out of EU": + $taxtype2 = "G"; + break; + case "Outside the tax scope": + $taxtype2 = "O"; + break; + case "Canary Islands": + $taxtype2 = "L"; + break; + case "Ceuta / Melila": + $taxtype2 = "M"; + break; + } + $taxtype3 = ""; + switch ($company->tax_type3){ + case "Sales Tax": + $taxtype3 = "S"; + break; + case "ZeroRate": + $taxtype3 = "Z"; + break; + case "Tax Exempt": + $taxtype3 = "E"; + break; + case "Reversal of tax liabilty": + $taxtype3 = "AE"; + break; + case "intra-community delivery": + $taxtype3 = "K"; + break; + case "Out of EU": + $taxtype3 = "G"; + break; + case "Outside the tax scope": + $taxtype3 = "O"; + break; + case "Canary Islands": + $taxtype3 = "L"; + break; + case "Ceuta / Melila": + $taxtype3 = "M"; + break; + } $taxamount_1 = $taxamount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; $netprice = 0.0; $chargetotalamount = $discount = 0.0; @@ -117,19 +208,19 @@ class CreateXInvoice implements ShouldQueue // According to european law, each artical can only have one tax percentage if ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ if ($invoice->tax_name1 != null && $invoice->tax_name2 == null && $invoice->tax_name3 == null){ - $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate1); + $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $invoice->tax_rate1); $taxnet_1 += $item->line_total - $discountamount; $taxamount_1 += $item->tax_amount; } elseif ($invoice->tax_name1 == null && $invoice->tax_name2 != null && $invoice->tax_name3 == null){ $taxnet_2 += $item->line_total - $discountamount; $taxamount_2 += $item->tax_amount; - $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate2); + $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $invoice->tax_rate2); } elseif ($invoice->tax_name1 == null && $invoice->tax_name2 == null && $invoice->tax_name3 != null){ $taxnet_3 += $item->line_total - $discountamount; $taxamount_3 += $item->tax_amount; - $xrechnung->addDocumentPositionTax('S', 'VAT', $invoice->tax_rate3); + $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $invoice->tax_rate3); } else{ nlog("Can't add correct tax position"); @@ -139,17 +230,17 @@ class CreateXInvoice implements ShouldQueue if ($item->tax_name1 != "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ $taxnet_1 += $item->line_total - $discountamount; $taxamount_1 += $item->tax_amount; - $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate1); + $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $item->tax_rate1); } elseif ($item->tax_name1 == "" && $item->tax_name2 != "" && $item->tax_name3 == ""){ $taxnet_2 += $item->line_total - $discountamount; $taxamount_2 += $item->tax_amount; - $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate2); + $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $item->tax_rate2); } elseif ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 != ""){ $taxnet_3 += $item->line_total - $discountamount; $taxamount_3 += $item->tax_amount; - $xrechnung->addDocumentPositionTax('S', 'VAT', $item->tax_rate3); + $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $item->tax_rate3); } } } @@ -188,13 +279,13 @@ class CreateXInvoice implements ShouldQueue $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, 0.0); } if ($taxnet_1 > 0){ - $xrechnung->addDocumentTax("S", "VAT", $taxnet_1, $taxamount_1, $invoice->tax_rate1); + $xrechnung->addDocumentTax($taxtype1, "VAT", $taxnet_1, $taxamount_1, $invoice->tax_rate1); } if ($taxnet_2 > 0) { - $xrechnung->addDocumentTax("S", "VAT", $taxnet_2, $taxamount_2, $invoice->tax_rate2); + $xrechnung->addDocumentTax($taxtype2, "VAT", $taxnet_2, $taxamount_2, $invoice->tax_rate2); } if ($taxnet_3 > 0) { - $xrechnung->addDocumentTax("S", "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); + $xrechnung->addDocumentTax($taxtype3, "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); From 8da6911e0e72e1abf9d7312ec9bc2f34c515f730 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 14 Mar 2023 21:41:43 +0100 Subject: [PATCH 13/35] Simplified switch statement --- app/Jobs/Invoice/CreateXInvoice.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index a65e8272d642..d0d61fe5cd41 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -88,9 +88,6 @@ class CreateXInvoice implements ShouldQueue // Create line items and calculate taxes $taxtype1 = ""; switch ($company->tax_type1){ - case "Sales Tax": - $taxtype1 = "S"; - break; case "ZeroRate": $taxtype1 = "Z"; break; @@ -115,12 +112,12 @@ class CreateXInvoice implements ShouldQueue case "Ceuta / Melila": $taxtype1 = "M"; break; + default: + $taxtype1 = "S"; + break; } $taxtype2 = ""; switch ($company->tax_type2){ - case "Sales Tax": - $taxtype2 = "S"; - break; case "ZeroRate": $taxtype2 = "Z"; break; @@ -145,12 +142,12 @@ class CreateXInvoice implements ShouldQueue case "Ceuta / Melila": $taxtype2 = "M"; break; + default: + $taxtype2 = "S"; + break; } $taxtype3 = ""; switch ($company->tax_type3){ - case "Sales Tax": - $taxtype3 = "S"; - break; case "ZeroRate": $taxtype3 = "Z"; break; @@ -175,6 +172,9 @@ class CreateXInvoice implements ShouldQueue case "Ceuta / Melila": $taxtype3 = "M"; break; + default: + $taxtype3 = "S"; + break; } $taxamount_1 = $taxamount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; $netprice = 0.0; From 0e74d6e03d64d06eefb622f09c06c4952f2efd81 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 3 Apr 2023 14:34:47 +0200 Subject: [PATCH 14/35] Created API for XInvoice --- app/Http/Controllers/InvoiceController.php | 71 +++++++++++++++++++++- app/Jobs/Invoice/CreateXInvoice.php | 1 - app/Services/Invoice/InvoiceService.php | 6 +- routes/api.php | 3 +- 4 files changed, 74 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index fb7cde006ec1..479e647a7f32 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -535,7 +535,7 @@ class InvoiceController extends BaseController if (Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) { return response(['message' => 'Please verify your account to send emails.'], 400); } - + $invoices = Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get(); if (! $invoices) { @@ -678,7 +678,7 @@ class InvoiceController extends BaseController case 'clone_to_invoice': $invoice = CloneInvoiceFactory::create($invoice, auth()->user()->id); return $this->itemResponse($invoice); - + case 'clone_to_quote': $quote = CloneInvoiceToQuoteFactory::create($invoice, auth()->user()->id); @@ -860,6 +860,73 @@ class InvoiceController extends BaseController }, basename($file), $headers); } + /** + * @OA\Get( + * path="/api/v1/invoice/{invitation_key}/download_xinvoice", + * operationId="downloadXInvoice", + * tags={"invoices"}, + * summary="Download a specific x-invoice by invitation key", + * description="Downloads a specific x-invoice", + * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="invitation_key", + * in="path", + * description="The Invoice Invitation Key", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the x-invoice pdf", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + * @param $invitation_key + * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + */ + public function downloadXInvoice($invitation_key) + { + $invitation = $this->invoice_repo->getInvitationByKey($invitation_key); + + if (! $invitation) { + return response()->json(['message' => 'no record found'], 400); + } + + $contact = $invitation->contact; + $invoice = $invitation->invoice; + + $file = $invoice->service()->getXInvoice($contact); + + $headers = ['Content-Type' => 'application/xml']; + + if (request()->input('inline') == 'true') { + $headers = array_merge($headers, ['Content-Disposition' => 'inline']); + } + + return response()->streamDownload(function () use ($file) { + echo Storage::get($file); + }, basename($file), $headers); + } + /** * @OA\Get( * path="/api/v1/invoices/{id}/delivery_note", diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index d0d61fe5cd41..ee802533a337 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -12,7 +12,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Storage; -use function Symfony\Component\String\b; class CreateXInvoice implements ShouldQueue diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 68cb3101ef32..e37e5597bb64 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -187,7 +187,7 @@ class InvoiceService public function getXInvoice($contact = null) { - return (new GetInvoiceXInvoice($this->invoice))->run(); + return (new GetInvoiceXInvoice($this->invoice, $contact))->run(); } public function sendEmail($contact = null) @@ -448,7 +448,7 @@ class InvoiceService if ($force) { $this->invoice->invitations->each(function ($invitation) { (new CreateEntityPdf($invitation))->handle(); - // Add XInvoice + (new CreateXInvoice($invitation))->handle(); }); return $this; @@ -456,7 +456,7 @@ class InvoiceService $this->invoice->invitations->each(function ($invitation) { CreateEntityPdf::dispatch($invitation); - // Add XInvoice + CreateXInvoice::dispatch($invitation); }); } catch (\Exception $e) { nlog('failed creating invoices in Touch PDF'); diff --git a/routes/api.php b/routes/api.php index daf2db3a84a1..4dd7b33e6e30 100644 --- a/routes/api.php +++ b/routes/api.php @@ -206,6 +206,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action'); Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload'); Route::get('invoice/{invitation_key}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf'); + Route::get('invoice/{invitation_key}/download_xinvoice', [InvoiceController::class, 'downloadXInvoice_pdf'])->name('invoices.downloadXInvoice'); Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk'); Route::post('invoices/update_reminders', [InvoiceController::class, 'update_reminders'])->name('invoices.update_reminders'); @@ -317,7 +318,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::post('verify', [TwilioController::class, 'generate'])->name('verify.generate')->middleware('throttle:100,1'); Route::post('verify/confirm', [TwilioController::class, 'confirm'])->name('verify.confirm'); - + Route::resource('vendors', VendorController::class); // name = (vendors. index / create / show / update / destroy / edit Route::post('vendors/bulk', [VendorController::class, 'bulk'])->name('vendors.bulk'); Route::put('vendors/{vendor}/upload', [VendorController::class, 'upload']); From 1c339cd2082d31f717d8c2348e3b5c75d353461f Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 3 Apr 2023 17:55:40 +0200 Subject: [PATCH 15/35] More fixes --- app/Jobs/Invoice/CreateXInvoice.php | 4 ++-- app/Models/Client.php | 6 ++++++ app/Services/Invoice/GetInvoiceXInvoice.php | 2 +- app/Services/Invoice/InvoiceService.php | 11 +++++++++-- routes/client.php | 3 ++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index ee802533a337..964d3a73b35b 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -69,7 +69,7 @@ class CreateXInvoice implements ShouldQueue ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) ->addDocumentNote($invoice->public_notes) ->setDocumentSupplyChainEvent(date_create($invoice->date)) - ->setDocumentSeller($company->name) +// ->setDocumentSeller($company->name) ->setDocumentSellerAddress($company->address1, "", "", $company->postal_code, $company->city, $company->country->country->iso_3166_2) ->setDocumentBuyer($client->name, $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2) @@ -286,7 +286,7 @@ class CreateXInvoice implements ShouldQueue if ($taxnet_3 > 0) { $xrechnung->addDocumentTax($taxtype3, "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } - $xrechnung->writeFile(explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); + $xrechnung->writeFile(explode(".", $client->xinvoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()); $disk = config('filesystems.default'); diff --git a/app/Models/Client.php b/app/Models/Client.php index 25b553f25f4c..d817e9b4c69f 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -732,6 +732,12 @@ class Client extends BaseModel implements HasLocalePreference return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/'; } + public function xinvoice_filepath($invitation) + { + $contact_key = $invitation->contact->contact_key; + + return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/xinvoice/'; + } public function quote_filepath($invitation) { diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php index a4f2e0e37e9b..336f1a6b8cbd 100644 --- a/app/Services/Invoice/GetInvoiceXInvoice.php +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -38,7 +38,7 @@ class GetInvoiceXInvoice extends AbstractService $invitation = $this->invoice->invitations->first(); } - $path = $this->invoice->client->invoice_filepath($invitation); + $path = $this->invoice->client->xinvoice_filepath($invitation); $file_path = $path.$this->invoice->numberFormatter().'-xinvoice.xml'; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index e37e5597bb64..d28ee76942cb 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -19,6 +19,7 @@ use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\CompanyGateway; use App\Models\Expense; use App\Models\Invoice; +use App\Models\InvoiceInvitation; use App\Models\Payment; use App\Models\Task; use App\Utils\Ninja; @@ -448,7 +449,10 @@ class InvoiceService if ($force) { $this->invoice->invitations->each(function ($invitation) { (new CreateEntityPdf($invitation))->handle(); - (new CreateXInvoice($invitation))->handle(); + if ($invitation instanceof InvoiceInvitation) + { + (new CreateXInvoice($invitation->invoice))->handle(); + } }); return $this; @@ -456,7 +460,10 @@ class InvoiceService $this->invoice->invitations->each(function ($invitation) { CreateEntityPdf::dispatch($invitation); - CreateXInvoice::dispatch($invitation); + if ($invitation instanceof InvoiceInvitation) + { + CreateXInvoice::dispatch($invitation->invoice); + } }); } catch (\Exception $e) { nlog('failed creating invoices in Touch PDF'); diff --git a/routes/client.php b/routes/client.php index b77efaf61c7b..f40906d2a3bf 100644 --- a/routes/client.php +++ b/routes/client.php @@ -96,7 +96,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'domain_db','check_clie Route::resource('documents', App\Http\Controllers\ClientPortal\DocumentController::class)->only(['index', 'show']); Route::get('subscriptions/{recurring_invoice}/plan_switch/{target}', [App\Http\Controllers\ClientPortal\SubscriptionPlanSwitchController::class, 'index'])->name('subscription.plan_switch'); - + Route::get('subscriptions/{recurring_invoice}', [SubscriptionController::class, 'show'])->middleware('portal_enabled')->name('subscriptions.show'); Route::get('subscriptions', [SubscriptionController::class, 'index'])->middleware('portal_enabled')->name('subscriptions.index'); @@ -124,6 +124,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('credit/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'creditRouter']); Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key'); Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key'); + Route::get('invoice/{invitation_key}/download_xinvoice', [InvoiceController::class, 'downloadXInvoice'])->name('invoice.download_xinvoice'); Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key'); Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key'); Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload']); From 73ac25072f4ac2631d86a73a084588af3df5f369 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 3 Apr 2023 21:00:47 +0200 Subject: [PATCH 16/35] More fixes --- app/Jobs/Invoice/CreateXInvoice.php | 30 +++++++++++-------- app/Services/Invoice/GetInvoiceXInvoice.php | 4 +-- ...156872_add_leitweg_id_to_clients_table.php | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 964d3a73b35b..b0d3248bc4f0 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -3,6 +3,7 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; +use App\Models\Country; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; @@ -69,20 +70,20 @@ class CreateXInvoice implements ShouldQueue ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) ->addDocumentNote($invoice->public_notes) ->setDocumentSupplyChainEvent(date_create($invoice->date)) -// ->setDocumentSeller($company->name) - ->setDocumentSellerAddress($company->address1, "", "", $company->postal_code, $company->city, $company->country->country->iso_3166_2) + ->setDocumentSeller($company->getSetting('name')) + ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), "Germany") //Country::query()->where('id', $company->getSetting("country_id"))->first() ->setDocumentBuyer($client->name, $client->number) - ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->country->iso_3166_2) + ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, "Germany") // $client->country->country->iso_3166_2 ->setDocumentBuyerReference($client->leitweg_id) - ->setDocumentBuyerContact($client->primary_contact->first_name." ".$client->primary_contact->last_name, "", $client->primary_contact->phone, "", $client->primary_contact->email) + //->setDocumentBuyerContact($client->primary_contact()->first_name." ".$client->primary_contact->last_name, "", $client->primary_contact->phone, "", $client->primary_contact->email) ->setDocumentBuyerOrderReferencedDocument($invoice->po_number) ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($invoice->date)->diff(date_create($invoice->due_date))->format("%d"), 'paydate' => $invoice->due_date])); - if (str_contains($company->vat_number, "/")){ - $xrechnung->addDocumentSellerTaxRegistration("FC", $company->vat_number); + if (str_contains($company->getSetting('vat_number'), "/")){ + $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); } else { - $xrechnung->addDocumentSellerTaxRegistration("VA", $company->vat_number); + $xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); } // Create line items and calculate taxes $taxtype1 = ""; @@ -286,18 +287,21 @@ class CreateXInvoice implements ShouldQueue if ($taxnet_3 > 0) { $xrechnung->addDocumentTax($taxtype3, "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); } - $xrechnung->writeFile(explode(".", $client->xinvoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"); - - $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()); $disk = config('filesystems.default'); + if(!Storage::exists($client->xinvoice_filepath($invoice->invitations->first()))){ + Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); + } + $xrechnung->writeFile(Storage::disk($disk)->path($client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); + $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName(); + $file = Storage::disk($disk)->exists($filepath_pdf); if ($file) { - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $filepath_pdf); + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, Storage::disk($disk)->path($filepath_pdf)); $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument($client->invoice_filepath($invoice->invitations->first())); + $pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf)); } - return explode(".", $client->invoice_filepath($invoice->invitations->first()))[0] . "-xinvoice.xml"; + return $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); } private function getItemTaxable($item, $invoice_total): float { diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php index 336f1a6b8cbd..70c4ad76d6a5 100644 --- a/app/Services/Invoice/GetInvoiceXInvoice.php +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -38,9 +38,7 @@ class GetInvoiceXInvoice extends AbstractService $invitation = $this->invoice->invitations->first(); } - $path = $this->invoice->client->xinvoice_filepath($invitation); - - $file_path = $path.$this->invoice->numberFormatter().'-xinvoice.xml'; + $file_path = $this->invoice->client->xinvoice_filepath($this->invoice->invitations->first()). $this->invoice->getFileName("xml"); // $disk = 'public'; $disk = config('filesystems.default'); diff --git a/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php b/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php index 4bc691615a47..761cb6404fa5 100644 --- a/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php +++ b/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php @@ -15,7 +15,7 @@ return new class extends Migration { Schema::table('clients', function (Blueprint $table) { - $table->string('leitweg_idf')->default(null); + $table->string('leitweg_id')->default(null); }); Schema::table('companies', function (Blueprint $table) { $table->boolean('use_xinvoice')->default(false); From dff4f762d4775545246f186065f53364f6ffc410 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 3 Apr 2023 21:18:07 +0200 Subject: [PATCH 17/35] More fixes --- app/Services/Invoice/InvoiceService.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index d28ee76942cb..d1831581b295 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -364,12 +364,12 @@ class InvoiceService $this->invoice->invitations->each(function ($invitation) { try { - if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml')) { - Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml'); + if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->xinvoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + Storage::disk(config('filesystems.default'))->delete($this->invoice->client->xinvoice_filepath($invitation).$this->invoice->getFileName("xml")); } - if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml')) { - Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->numberFormatter().'-xinvoice.xml'); + if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->getFileName("xml")); } } catch (\Exception $e) { nlog($e->getMessage()); From 55bb506d4ed91f817e485cfec0bbc63dda03f2e3 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 3 Apr 2023 21:18:20 +0200 Subject: [PATCH 18/35] Attach XInvoice to email --- app/Mail/TemplateEmail.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 6d8dd1ffaac8..1f9ffad3ec4e 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -18,6 +18,7 @@ use App\Services\PdfMaker\Designs\Utilities\DesignHelpers; use App\Utils\HtmlEngine; use App\Utils\Ninja; use Illuminate\Mail\Mailable; +use Illuminate\Support\Facades\Storage; class TemplateEmail extends Mailable { @@ -152,6 +153,11 @@ class TemplateEmail extends Mailable $this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml')); } } + if ($this->invitation && $this->invitation->invoice && $company->use_xinvoice && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + $this->invitation->invoice->service()->getXInvoice($this->invitation->contact); + $disk = config('filesystems.default'); + $this->attach(Storage::disk($disk)->path($this->invitation->invoice->client->xinvoice_filepath($this->invitation->invoice->invitations->first()) . $this->invitation->invoice->getFileName("xml"))); + } return $this; } From f2c38bb94885140a734635fe4a843d8c46ca44f9 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 4 Apr 2023 08:58:01 +0200 Subject: [PATCH 19/35] Add Xinvoice to E-Mail --- .gitignore | 3 ++- app/Jobs/Invoice/CreateXInvoice.php | 16 +++++++------- app/Jobs/Invoice/ZipInvoices.php | 2 +- app/Services/Email/EmailDefaults.php | 23 ++++++++++++++------- app/Services/Invoice/GetInvoiceXInvoice.php | 2 +- app/Services/Invoice/InvoiceService.php | 4 ++-- 6 files changed, 30 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 8c1625524ae2..dff90a49360a 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ public/test.pdf public/storage/test.pdf /Modules _ide_helper_models.php -_ide_helper.php \ No newline at end of file +_ide_helper.php +/composer.phar diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index b0d3248bc4f0..454af161620a 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -21,9 +21,10 @@ class CreateXInvoice implements ShouldQueue public Invoice $invoice; - public function __construct(Invoice $invoice) + public function __construct(Invoice $invoice, bool $alterPDF) { $this->invoice = $invoice; + $this->alterpdf = $alterPDF; } /** @@ -294,12 +295,13 @@ class CreateXInvoice implements ShouldQueue $xrechnung->writeFile(Storage::disk($disk)->path($client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); $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)); + if ($this->alterpdf){ + $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->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); } diff --git a/app/Jobs/Invoice/ZipInvoices.php b/app/Jobs/Invoice/ZipInvoices.php index 064d42f7f496..3eb9bd21823e 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))->handle(); + (new CreateXInvoice($invoice, false))->handle(); } }); diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 476ab592cdd8..5ab465de7929 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -11,6 +11,7 @@ namespace App\Services\Email; +use App\Jobs\Invoice\CreateXInvoice; use App\Models\Task; use App\Utils\Ninja; use App\Models\Quote; @@ -55,7 +56,7 @@ class EmailDefaults public function __construct(protected Email $email) { } - + /** * Entry point for generating * the defaults for the email object @@ -66,7 +67,7 @@ class EmailDefaults { $this->settings = $this->email->email_object->settings; - $this->setLocale() + $this->setLocale() ->setFrom() ->setTo() ->setTemplate() @@ -76,7 +77,7 @@ class EmailDefaults ->setBcc() ->setAttachments() ->setVariables(); - + return $this->email->email_object; } @@ -169,7 +170,7 @@ class EmailDefaults // Default template to be used $this->email->email_object->body = EmailTemplateDefaults::getDefaultTemplate($this->email->email_object->email_template_body, $this->locale); } - + if ($this->template == 'email.template.custom') { $this->email->email_object->body = (str_replace('$body', $this->email->email_object->body, $this->email->email_object->settings->email_style_custom)); } @@ -214,7 +215,7 @@ class EmailDefaults public function setVariables(): self { $this->email->email_object->body = strtr($this->email->email_object->body, $this->email->email_object->variables); - + $this->email->email_object->subject = strtr($this->email->email_object->subject, $this->email->email_object->variables); if ($this->template != 'custom') { @@ -243,7 +244,7 @@ class EmailDefaults foreach ($bccs as $bcc) { $bcc_array[] = new Address($bcc); } - + $this->email->email_object->bcc = array_merge($this->email->email_object->bcc, $bcc_array); return $this; @@ -256,7 +257,7 @@ class EmailDefaults private function buildCc() { return [ - + ]; } @@ -289,6 +290,7 @@ class EmailDefaults $this->email->email_object->entity instanceof Quote || $this->email->email_object->entity instanceof Credit)) { + // TODO: Alter this to include XInvoice $pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle()); $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]); @@ -303,6 +305,11 @@ class EmailDefaults $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($ubl_string), 'name' => $this->email->email_object->entity->getFileName('xml')]]); } } + /** XInvoice xml file */ + if ($this->email->email_object->company->use_xinvoice && $this->email->email_object->entity instanceof Invoice) { + $xinvoice_path = (new CreateXInvoice($this->email->email_object->entity, false))->handle(); + $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"]]); + } if(!$this->email->email_object->settings->document_email_attachment) return $this; @@ -324,7 +331,7 @@ class EmailDefaults if ($this->email->email_object->entity instanceof Invoice) { $expense_ids = []; $task_ids = []; - + foreach ($this->email->email_object->entity->line_items as $item) { if (property_exists($item, 'expense_id')) { $expense_ids[] = $item->expense_id; diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php index 70c4ad76d6a5..f5d279d34fb3 100644 --- a/app/Services/Invoice/GetInvoiceXInvoice.php +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -46,7 +46,7 @@ class GetInvoiceXInvoice extends AbstractService $file = Storage::disk($disk)->exists($file_path); if (! $file) { - $file_path = (new CreateXInvoice($this->invoice))->handle(); + $file_path = (new CreateXInvoice($this->invoice, false))->handle(); } return $file_path; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index d1831581b295..8b180ef12c36 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -451,7 +451,7 @@ class InvoiceService (new CreateEntityPdf($invitation))->handle(); if ($invitation instanceof InvoiceInvitation) { - (new CreateXInvoice($invitation->invoice))->handle(); + (new CreateXInvoice($invitation->invoice, true))->handle(); } }); @@ -462,7 +462,7 @@ class InvoiceService CreateEntityPdf::dispatch($invitation); if ($invitation instanceof InvoiceInvitation) { - CreateXInvoice::dispatch($invitation->invoice); + CreateXInvoice::dispatch($invitation->invoice, true); } }); } catch (\Exception $e) { From 073fee4c5b9de780f072fc55989143a7073c2105 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 4 Apr 2023 09:18:22 +0200 Subject: [PATCH 20/35] Add Xinvoice to E-Mail --- app/Jobs/Invoice/CreateXInvoice.php | 20 ++++++++++++++------ app/Services/Email/EmailDefaults.php | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 454af161620a..5460af671c66 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -21,10 +21,11 @@ class CreateXInvoice implements ShouldQueue public Invoice $invoice; - public function __construct(Invoice $invoice, bool $alterPDF) + public function __construct(Invoice $invoice, bool $alterPDF, string $custompdfpath = "") { $this->invoice = $invoice; $this->alterpdf = $alterPDF; + $this->custompdfpath = $custompdfpath; } /** @@ -293,14 +294,21 @@ class CreateXInvoice implements ShouldQueue Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); } $xrechnung->writeFile(Storage::disk($disk)->path($client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); - $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName(); if ($this->alterpdf){ - $file = Storage::disk($disk)->exists($filepath_pdf); - if ($file) { - $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, Storage::disk($disk)->path($filepath_pdf)); + if ($this->custompdfpath != ""){ + $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $this->custompdfpath); $pdfBuilder->generateDocument(); - $pdfBuilder->saveDocument(Storage::disk($disk)->path($filepath_pdf)); + $pdfBuilder->saveDocument($this->custompdfpath); + } + 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->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 5ab465de7929..4820c4f628b0 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -13,6 +13,7 @@ namespace App\Services\Email; use App\Jobs\Invoice\CreateXInvoice; use App\Models\Task; +use App\Services\Invoice\GetInvoiceXInvoice; use App\Utils\Ninja; use App\Models\Quote; use App\Models\Credit; @@ -292,8 +293,17 @@ class EmailDefaults // TODO: Alter this to include XInvoice $pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle()); + if ($this->email->email_object->company->use_xinvoice && $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(); + $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"]]); + } + else { + $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]); + } - $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($pdf), 'name' => $this->email->email_object->entity->numberFormatter().'.pdf']]); } @@ -307,7 +317,7 @@ class EmailDefaults } /** XInvoice xml file */ if ($this->email->email_object->company->use_xinvoice && $this->email->email_object->entity instanceof Invoice) { - $xinvoice_path = (new CreateXInvoice($this->email->email_object->entity, false))->handle(); + $xinvoice_path = (new GetInvoiceXInvoice($this->email->email_object->entity))->run(); $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"]]); } From e8a12816a929283c833ded4527dbdeed44c6dcf5 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 4 Apr 2023 09:18:37 +0200 Subject: [PATCH 21/35] Add Xinvoice to E-Mail --- app/Services/Email/EmailDefaults.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 4820c4f628b0..10defe47a4a1 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -291,7 +291,6 @@ class EmailDefaults $this->email->email_object->entity instanceof Quote || $this->email->email_object->entity instanceof Credit)) { - // TODO: Alter this to include XInvoice $pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle()); if ($this->email->email_object->company->use_xinvoice && $this->email->email_object->entity instanceof Invoice) { $tempfile = tmpfile(); From dabfe543f1527b3c66e334ddb18b01efd6869676 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Tue, 4 Apr 2023 11:46:51 +0200 Subject: [PATCH 22/35] More fixes --- app/Jobs/Invoice/CreateXInvoice.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 5460af671c66..46147bf819a6 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -73,11 +73,11 @@ class CreateXInvoice implements ShouldQueue ->addDocumentNote($invoice->public_notes) ->setDocumentSupplyChainEvent(date_create($invoice->date)) ->setDocumentSeller($company->getSetting('name')) - ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), "Germany") //Country::query()->where('id', $company->getSetting("country_id"))->first() + ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2) ->setDocumentBuyer($client->name, $client->number) - ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, "Germany") // $client->country->country->iso_3166_2 + ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) ->setDocumentBuyerReference($client->leitweg_id) - //->setDocumentBuyerContact($client->primary_contact()->first_name." ".$client->primary_contact->last_name, "", $client->primary_contact->phone, "", $client->primary_contact->email) + ->setDocumentBuyerContact($client->primary_contact()->first()->first_name." ".$client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) ->setDocumentBuyerOrderReferencedDocument($invoice->po_number) ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($invoice->date)->diff(date_create($invoice->due_date))->format("%d"), 'paydate' => $invoice->due_date])); From d079a02bff9992787d249445ab3900ce76a5a935 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 11:22:41 +0200 Subject: [PATCH 23/35] Refactoring to use the $invoice->calc() method --- app/Helpers/Invoice/InvoiceSum.php | 5 + app/Helpers/Invoice/InvoiceSumInclusive.php | 5 + app/Jobs/Invoice/CreateXInvoice.php | 184 +++----------------- 3 files changed, 39 insertions(+), 155 deletions(-) diff --git a/app/Helpers/Invoice/InvoiceSum.php b/app/Helpers/Invoice/InvoiceSum.php index 3510804b5917..8f0d734521e4 100644 --- a/app/Helpers/Invoice/InvoiceSum.php +++ b/app/Helpers/Invoice/InvoiceSum.php @@ -294,6 +294,11 @@ class InvoiceSum return $this->total; } + public function getTotalSurcharges() + { + return $this->total_custom_values; + } + public function setTaxMap() { if ($this->invoice->is_amount_discount == true) { diff --git a/app/Helpers/Invoice/InvoiceSumInclusive.php b/app/Helpers/Invoice/InvoiceSumInclusive.php index d2af75e18688..ce177a6e850f 100644 --- a/app/Helpers/Invoice/InvoiceSumInclusive.php +++ b/app/Helpers/Invoice/InvoiceSumInclusive.php @@ -175,6 +175,11 @@ class InvoiceSumInclusive return $this; } + public function getTotalSurcharges() + { + return $this->total_custom_values; + } + public function getRecurringInvoice() { $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision); diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 46147bf819a6..f83f75c1f933 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -70,7 +70,6 @@ class CreateXInvoice implements ShouldQueue $xrechnung ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) - ->addDocumentNote($invoice->public_notes) ->setDocumentSupplyChainEvent(date_create($invoice->date)) ->setDocumentSeller($company->getSetting('name')) ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2) @@ -78,8 +77,13 @@ class CreateXInvoice implements ShouldQueue ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) ->setDocumentBuyerReference($client->leitweg_id) ->setDocumentBuyerContact($client->primary_contact()->first()->first_name." ".$client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) - ->setDocumentBuyerOrderReferencedDocument($invoice->po_number) ->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 (str_contains($company->getSetting('vat_number'), "/")){ $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); @@ -87,7 +91,11 @@ class CreateXInvoice implements ShouldQueue else { $xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); } - // Create line items and calculate taxes + + $invoicingdata = $invoice->calc(); + + + //Create line items and calculate taxes $taxtype1 = ""; switch ($company->tax_type1){ case "ZeroRate": @@ -178,11 +186,6 @@ class CreateXInvoice implements ShouldQueue $taxtype3 = "S"; break; } - $taxamount_1 = $taxamount_2 = $taxamount_3 = $taxnet_1 = $taxnet_2 = $taxnet_3 = 0.0; - $netprice = 0.0; - $chargetotalamount = $discount = 0.0; - $taxable = $this->getTaxable(); - foreach ($invoice->line_items as $index => $item){ $xrechnung->addNewPosition($index) ->setDocumentPositionProductDetails($item->notes) @@ -194,34 +197,15 @@ class CreateXInvoice implements ShouldQueue else{ $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); } - $netprice += $this->getItemTaxable($item, $taxable); - $discountamount = 0.0; - if ($item->discount > 0){ - if ($invoice->is_amount_discount){ - $discountamount = $item->discount; - $discount += $item->discount; - } - else { - $discountamount = $item->line_total * ($item->discount / 100); - $discount += $item->line_total * ($item->discount / 100); - } - } - // According to european law, each artical can only have one tax percentage - if ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ - if ($invoice->tax_name1 != null && $invoice->tax_name2 == null && $invoice->tax_name3 == null){ + if (empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3)){ + if (!empty($invoice->tax_name1)){ $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $invoice->tax_rate1); - $taxnet_1 += $item->line_total - $discountamount; - $taxamount_1 += $item->tax_amount; } - elseif ($invoice->tax_name1 == null && $invoice->tax_name2 != null && $invoice->tax_name3 == null){ - $taxnet_2 += $item->line_total - $discountamount; - $taxamount_2 += $item->tax_amount; + elseif (!empty($invoice->tax_name2)){ $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $invoice->tax_rate2); } - elseif ($invoice->tax_name1 == null && $invoice->tax_name2 == null && $invoice->tax_name3 != null){ - $taxnet_3 += $item->line_total - $discountamount; - $taxamount_3 += $item->tax_amount; + elseif (!empty($invoice->tax_name3)){ $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $invoice->tax_rate3); } else{ @@ -230,65 +214,37 @@ class CreateXInvoice implements ShouldQueue } else { if ($item->tax_name1 != "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ - $taxnet_1 += $item->line_total - $discountamount; - $taxamount_1 += $item->tax_amount; $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $item->tax_rate1); } elseif ($item->tax_name1 == "" && $item->tax_name2 != "" && $item->tax_name3 == ""){ - $taxnet_2 += $item->line_total - $discountamount; - $taxamount_2 += $item->tax_amount; $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $item->tax_rate2); } elseif ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 != ""){ - $taxnet_3 += $item->line_total - $discountamount; - $taxamount_3 += $item->tax_amount; $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $item->tax_rate3); } } } - // Calculate global surcharges - if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) { - $chargetotalamount += $this->invoice->custom_surcharge1; - } - - if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) { - $chargetotalamount += $this->invoice->custom_surcharge2; - } - - if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) { - $chargetotalamount += $this->invoice->custom_surcharge3; - } - - if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) { - $chargetotalamount += $this->invoice->custom_surcharge4; - } - - // Calculate global discounts - if ($invoice->disount > 0){ - if ($invoice->is_amount_discount){ - $discount += $invoice->discount; - } - else { - $discount += $invoice->amount * ($invoice->discount / 100); - } - } - if ($invoice->isPartial()){ - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, $invoice->partial);} - else { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $netprice, $chargetotalamount, $discount, $taxable, $invoice->total_taxes, null, 0.0); + $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount-$invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial); + } else { + $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount-$invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); } - if ($taxnet_1 > 0){ - $xrechnung->addDocumentTax($taxtype1, "VAT", $taxnet_1, $taxamount_1, $invoice->tax_rate1); + + if (count($invoicingdata->getTaxMap()) > 0){ + $tax = explode(" ", $invoicingdata->getTaxMap()[0]["name"]); + $xrechnung->addDocumentTax($taxtype1, "VAT", $invoicingdata->getTaxMap()[0]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[0]["total"], explode("%", end($tax))[0]); } - if ($taxnet_2 > 0) { - $xrechnung->addDocumentTax($taxtype2, "VAT", $taxnet_2, $taxamount_2, $invoice->tax_rate2); + if (count($invoicingdata->getTaxMap()) > 1) { + $tax = explode(" ", $invoicingdata->getTaxMap()[1]["name"]); + $xrechnung->addDocumentTax($taxtype2, "VAT", $invoicingdata->getTaxMap()[1]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[1]["total"], explode("%", end($tax))[0]); } - if ($taxnet_3 > 0) { - $xrechnung->addDocumentTax($taxtype3, "VAT", $taxnet_3, $taxamount_3, $invoice->tax_rate3); + if (count($invoicingdata->getTaxMap()) > 2) { + $tax = explode(" ", $invoicingdata->getTaxMap()[2]["name"]); + $xrechnung->addDocumentTax($taxtype3, "VAT", $invoicingdata->getTaxMap()[2]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[2]["total"], explode("%", end($tax))[0]); } + $disk = config('filesystems.default'); if(!Storage::exists($client->xinvoice_filepath($invoice->invitations->first()))){ Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); @@ -313,86 +269,4 @@ class CreateXInvoice implements ShouldQueue } return $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); } - private function getItemTaxable($item, $invoice_total): float - { - $total = $item->quantity * $item->cost; - - if ($this->invoice->discount != 0) { - if ($this->invoice->is_amount_discount) { - if ($invoice_total + $this->invoice->discount != 0) { - $total -= $invoice_total ? ($total / ($invoice_total + $this->invoice->discount) * $this->invoice->discount) : 0; - } - } else { - $total *= (100 - $this->invoice->discount) / 100; - } - } - - if ($item->discount != 0) { - if ($this->invoice->is_amount_discount) { - $total -= $item->discount; - } else { - $total -= $total * $item->discount / 100; - } - } - - return round($total, 2); - } - - /** - * @return float - */ - private function getTaxable(): float - { - $total = 0.0; - - foreach ($this->invoice->line_items as $item) { - $line_total = $item->quantity * $item->cost; - - if ($item->discount != 0) { - if ($this->invoice->is_amount_discount) { - $line_total -= $item->discount; - } else { - $line_total -= $line_total * $item->discount / 100; - } - } - - $total += $line_total; - } - - if ($this->invoice->discount > 0) { - if ($this->invoice->is_amount_discount) { - $total -= $this->invoice->discount; - } else { - $total *= (100 - $this->invoice->discount) / 100; - $total = round($total, 2); - } - } - - if ($this->invoice->custom_surcharge1 && $this->invoice->custom_surcharge_tax1) { - $total += $this->invoice->custom_surcharge1; - } - - if ($this->invoice->custom_surcharge2 && $this->invoice->custom_surcharge_tax2) { - $total += $this->invoice->custom_surcharge2; - } - - if ($this->invoice->custom_surcharge3 && $this->invoice->custom_surcharge_tax3) { - $total += $this->invoice->custom_surcharge3; - } - - if ($this->invoice->custom_surcharge4 && $this->invoice->custom_surcharge_tax4) { - $total += $this->invoice->custom_surcharge4; - } - - return $total; - } - - public function taxAmount($taxable, $rate): float - { - if ($this->invoice->uses_inclusive_taxes) { - return round($taxable - ($taxable / (1 + ($rate / 100))), 2); - } else { - return round($taxable * ($rate / 100), 2); - } - } } From 3239431faa4de494731e37a9c9a0149c2de0ac27 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 11:39:54 +0200 Subject: [PATCH 24/35] Fixes some bugs and finished refactoring for calc method --- app/Jobs/Invoice/CreateXInvoice.php | 199 ++++++++++------------------ 1 file changed, 71 insertions(+), 128 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index f83f75c1f933..e36087732390 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -96,134 +96,44 @@ class CreateXInvoice implements ShouldQueue //Create line items and calculate taxes - $taxtype1 = ""; - switch ($company->tax_type1){ - case "ZeroRate": - $taxtype1 = "Z"; - break; - case "Tax Exempt": - $taxtype1 = "E"; - break; - case "Reversal of tax liabilty": - $taxtype1 = "AE"; - break; - case "intra-community delivery": - $taxtype1 = "K"; - break; - case "Out of EU": - $taxtype1 = "G"; - break; - case "Outside the tax scope": - $taxtype1 = "O"; - break; - case "Canary Islands": - $taxtype1 = "L"; - break; - case "Ceuta / Melila": - $taxtype1 = "M"; - break; - default: - $taxtype1 = "S"; - break; - } - $taxtype2 = ""; - switch ($company->tax_type2){ - case "ZeroRate": - $taxtype2 = "Z"; - break; - case "Tax Exempt": - $taxtype2 = "E"; - break; - case "Reversal of tax liabilty": - $taxtype2 = "AE"; - break; - case "intra-community delivery": - $taxtype2 = "K"; - break; - case "Out of EU": - $taxtype2 = "G"; - break; - case "Outside the tax scope": - $taxtype2 = "O"; - break; - case "Canary Islands": - $taxtype2 = "L"; - break; - case "Ceuta / Melila": - $taxtype2 = "M"; - break; - default: - $taxtype2 = "S"; - break; - } - $taxtype3 = ""; - switch ($company->tax_type3){ - case "ZeroRate": - $taxtype3 = "Z"; - break; - case "Tax Exempt": - $taxtype3 = "E"; - break; - case "Reversal of tax liabilty": - $taxtype3 = "AE"; - break; - case "intra-community delivery": - $taxtype3 = "K"; - break; - case "Out of EU": - $taxtype3 = "G"; - break; - case "Outside the tax scope": - $taxtype3 = "O"; - break; - case "Canary Islands": - $taxtype3 = "L"; - break; - case "Ceuta / Melila": - $taxtype3 = "M"; - break; - default: - $taxtype3 = "S"; - break; - } foreach ($invoice->line_items as $index => $item){ $xrechnung->addNewPosition($index) - ->setDocumentPositionProductDetails($item->notes) - ->setDocumentPositionGrossPrice($item->gross_line_total) - ->setDocumentPositionNetPrice($item->line_total); + ->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"); } - // According to european law, each artical can only have one tax percentage - if (empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3)){ - if (!empty($invoice->tax_name1)){ - $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $invoice->tax_rate1); - } - elseif (!empty($invoice->tax_name2)){ - $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $invoice->tax_rate2); - } - elseif (!empty($invoice->tax_name3)){ - $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $invoice->tax_rate3); - } - else{ - nlog("Can't add correct tax position"); - } + + // According to european law, each line item can only have one tax rate + if (!empty($item->tax_name1)){ + $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name1), 'VAT', $item->tax_rate1); } - else { - if ($item->tax_name1 != "" && $item->tax_name2 == "" && $item->tax_name3 == ""){ - $xrechnung->addDocumentPositionTax($taxtype1, 'VAT', $item->tax_rate1); - } - elseif ($item->tax_name1 == "" && $item->tax_name2 != "" && $item->tax_name3 == ""){ - $xrechnung->addDocumentPositionTax($taxtype2, 'VAT', $item->tax_rate2); - } - elseif ($item->tax_name1 == "" && $item->tax_name2 == "" && $item->tax_name3 != ""){ - $xrechnung->addDocumentPositionTax($taxtype3, 'VAT', $item->tax_rate3); - } + elseif (!empty($item->tax_name2)){ + $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); } - } + elseif (!empty($item->tax_name3)){ + $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); + } + else{ + nlog("Can't add correct tax position"); + } + + if (!empty($invoice->tax_name1)){ + $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); + } + elseif (!empty($invoice->tax_name2)){ + $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); + } + elseif (!empty($invoice->tax_name3)){ + $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); + } else{ + nlog("Can't add correct tax position");} + } + if ($invoice->isPartial()){ @@ -232,17 +142,15 @@ class CreateXInvoice implements ShouldQueue $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount-$invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); } - if (count($invoicingdata->getTaxMap()) > 0){ - $tax = explode(" ", $invoicingdata->getTaxMap()[0]["name"]); - $xrechnung->addDocumentTax($taxtype1, "VAT", $invoicingdata->getTaxMap()[0]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[0]["total"], explode("%", end($tax))[0]); + foreach ($invoicingdata->getTaxMap() as $item){ + $tax = explode(" ", $item["name"]); + $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"]/(explode("%", end($tax))[0]/100), $item["total"], explode("%", end($tax))[0]); + // TODO: Add correct tax type within getTaxType } - if (count($invoicingdata->getTaxMap()) > 1) { - $tax = explode(" ", $invoicingdata->getTaxMap()[1]["name"]); - $xrechnung->addDocumentTax($taxtype2, "VAT", $invoicingdata->getTaxMap()[1]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[1]["total"], explode("%", end($tax))[0]); - } - if (count($invoicingdata->getTaxMap()) > 2) { - $tax = explode(" ", $invoicingdata->getTaxMap()[2]["name"]); - $xrechnung->addDocumentTax($taxtype3, "VAT", $invoicingdata->getTaxMap()[2]["total"]/(explode("%", end($tax))[0]/100), $invoicingdata->getTaxMap()[2]["total"], explode("%", end($tax))[0]); + foreach ($invoicingdata->getTotalTaxMap() as $item){ + $tax = explode(" ", $item["name"]); + $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"]/(explode("%", end($tax))[0]/100), $item["total"], explode("%", end($tax))[0]); + // TODO: Add correct tax type within getTaxType } $disk = config('filesystems.default'); @@ -270,3 +178,38 @@ class CreateXInvoice implements ShouldQueue return $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); } } + +private function getTaxType(string $name) +{ + $taxtype = ""; + switch ($name){ + case "ZeroRate": + $taxtype = "Z"; + break; + case "Tax Exempt": + $taxtype = "E"; + break; + case "Reversal of tax liabilty": + $taxtype = "AE"; + break; + case "intra-community delivery": + $taxtype = "K"; + break; + case "Out of EU": + $taxtype = "G"; + break; + case "Outside the tax scope": + $taxtype = "O"; + break; + case "Canary Islands": + $taxtype = "L"; + break; + case "Ceuta / Melila": + $taxtype = "M"; + break; + default: + $taxtype = "S"; + break; + } + return $taxtype; +} From fd72b1dce564522731691499ef19805eb1f716c0 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 11:41:26 +0200 Subject: [PATCH 25/35] Fixed some bugs --- app/Jobs/Invoice/CreateXInvoice.php | 152 +++++++++++++--------------- 1 file changed, 72 insertions(+), 80 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index e36087732390..0b077096875f 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -40,7 +40,7 @@ class CreateXInvoice implements ShouldQueue $company = $invoice->company; $client = $invoice->client; $profile = ""; - switch ($company->xinvoice_type){ + switch ($company->xinvoice_type) { case "EN16931": $profile = ZugferdProfiles::PROFILE_EN16931; break; @@ -66,7 +66,7 @@ class CreateXInvoice implements ShouldQueue $profile = ZugferdProfiles::PROFILE_BASIC; break; } - $xrechnung = ZugferdDocumentBuilder::CreateNew($profile); + $xrechnung = ZugferdDocumentBuilder::CreateNew($profile); $xrechnung ->setDocumentInformation($invoice->number, "380", date_create($invoice->date), $invoice->client->getCurrencyCode()) @@ -76,19 +76,18 @@ class CreateXInvoice implements ShouldQueue ->setDocumentBuyer($client->name, $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) ->setDocumentBuyerReference($client->leitweg_id) - ->setDocumentBuyerContact($client->primary_contact()->first()->first_name." ".$client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) + ->setDocumentBuyerContact($client->primary_contact()->first()->first_name . " " . $client->primary_contact()->first()->last_name, "", $client->primary_contact()->first()->phone, "", $client->primary_contact()->first()->email) ->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)){ + if (!empty($invoice->public_notes)) { $xrechnung->addDocumentNote($invoice->public_notes); } - if(!empty($invoice->po_number)){ + if (!empty($invoice->po_number)) { $xrechnung->setDocumentBuyerOrderReferencedDocument($invoice->po_number); } - if (str_contains($company->getSetting('vat_number'), "/")){ + if (str_contains($company->getSetting('vat_number'), "/")) { $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); - } - else { + } else { $xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); } @@ -96,77 +95,70 @@ class CreateXInvoice implements ShouldQueue //Create line items and calculate taxes - foreach ($invoice->line_items as $index => $item){ + 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)){ + ->setDocumentPositionProductDetails($item->notes) + ->setDocumentPositionGrossPrice($item->gross_line_total) + ->setDocumentPositionNetPrice($item->line_total); + if (isset($item->task_id)) { $xrechnung->setDocumentPositionQuantity($item->quantity, "HUR"); - } - else{ + } else { $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); } // According to european law, each line item can only have one tax rate - if (!empty($item->tax_name1)){ + if (!empty($item->tax_name1)) { $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name1), 'VAT', $item->tax_rate1); - } - elseif (!empty($item->tax_name2)){ + } elseif (!empty($item->tax_name2)) { $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); - } - elseif (!empty($item->tax_name3)){ + } elseif (!empty($item->tax_name3)) { $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); - } - else{ + } else { nlog("Can't add correct tax position"); } - if (!empty($invoice->tax_name1)){ + if (!empty($invoice->tax_name1)) { $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); - } - elseif (!empty($invoice->tax_name2)){ + } elseif (!empty($invoice->tax_name2)) { $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); - } - elseif (!empty($invoice->tax_name3)){ + } elseif (!empty($invoice->tax_name3)) { $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); - } else{ - nlog("Can't add correct tax position");} + } else { + nlog("Can't add correct tax position"); } - - - - if ($invoice->isPartial()){ - $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount-$invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial); - } else { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount-$invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); } - foreach ($invoicingdata->getTaxMap() as $item){ + + if ($invoice->isPartial()) { + $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount - $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial); + } else { + $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount - $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); + } + + foreach ($invoicingdata->getTaxMap() as $item) { $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"]/(explode("%", end($tax))[0]/100), $item["total"], explode("%", end($tax))[0]); + $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); // TODO: Add correct tax type within getTaxType } - foreach ($invoicingdata->getTotalTaxMap() as $item){ + foreach ($invoicingdata->getTotalTaxMap() as $item) { $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"]/(explode("%", end($tax))[0]/100), $item["total"], explode("%", end($tax))[0]); + $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); // TODO: Add correct tax type within getTaxType } $disk = config('filesystems.default'); - if(!Storage::exists($client->xinvoice_filepath($invoice->invitations->first()))){ + if (!Storage::exists($client->xinvoice_filepath($invoice->invitations->first()))) { Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); } $xrechnung->writeFile(Storage::disk($disk)->path($client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); - if ($this->alterpdf){ - if ($this->custompdfpath != ""){ + if ($this->alterpdf) { + if ($this->custompdfpath != "") { $pdfBuilder = new ZugferdDocumentPdfBuilder($xrechnung, $this->custompdfpath); $pdfBuilder->generateDocument(); $pdfBuilder->saveDocument($this->custompdfpath); - } - else { - $filepath_pdf = $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName(); + } 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)); @@ -175,41 +167,41 @@ class CreateXInvoice implements ShouldQueue } } } - return $client->invoice_filepath($invoice->invitations->first()).$invoice->getFileName("xml"); + return $client->invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); } -} -private function getTaxType(string $name) -{ - $taxtype = ""; - switch ($name){ - case "ZeroRate": - $taxtype = "Z"; - break; - case "Tax Exempt": - $taxtype = "E"; - break; - case "Reversal of tax liabilty": - $taxtype = "AE"; - break; - case "intra-community delivery": - $taxtype = "K"; - break; - case "Out of EU": - $taxtype = "G"; - break; - case "Outside the tax scope": - $taxtype = "O"; - break; - case "Canary Islands": - $taxtype = "L"; - break; - case "Ceuta / Melila": - $taxtype = "M"; - break; - default: - $taxtype = "S"; - break; + private function getTaxType(string $name): string + { + $taxtype = ""; + switch ($name) { + case "ZeroRate": + $taxtype = "Z"; + break; + case "Tax Exempt": + $taxtype = "E"; + break; + case "Reversal of tax liabilty": + $taxtype = "AE"; + break; + case "intra-community delivery": + $taxtype = "K"; + break; + case "Out of EU": + $taxtype = "G"; + break; + case "Outside the tax scope": + $taxtype = "O"; + break; + case "Canary Islands": + $taxtype = "L"; + break; + case "Ceuta / Melila": + $taxtype = "M"; + break; + default: + $taxtype = "S"; + break; + } + return $taxtype; } - return $taxtype; } From 66193b6e6a859be3ee2eab62263db9cd4e7c08b8 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 11:43:37 +0200 Subject: [PATCH 26/35] More improvements --- app/Jobs/Invoice/CreateXInvoice.php | 60 ++++++++++------------------- 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 0b077096875f..8a414bb85038 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -20,6 +20,8 @@ class CreateXInvoice implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public Invoice $invoice; + private bool $alterpdf; + private string $custompdfpath; public function __construct(Invoice $invoice, bool $alterPDF, string $custompdfpath = "") { @@ -108,21 +110,21 @@ class CreateXInvoice implements ShouldQueue // According to european law, each line item can only have one tax rate if (!empty($item->tax_name1)) { - $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name1), 'VAT', $item->tax_rate1); + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name1), 'VAT', $item->tax_rate1); } elseif (!empty($item->tax_name2)) { - $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); } elseif (!empty($item->tax_name3)) { - $xrechnung->addDocumentPositionTax(getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); } else { nlog("Can't add correct tax position"); } if (!empty($invoice->tax_name1)) { - $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); } elseif (!empty($invoice->tax_name2)) { - $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); } elseif (!empty($invoice->tax_name3)) { - $xrechnung->addDocumentPositionTax(getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); } else { nlog("Can't add correct tax position"); } @@ -137,12 +139,12 @@ class CreateXInvoice implements ShouldQueue foreach ($invoicingdata->getTaxMap() as $item) { $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); + $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 } foreach ($invoicingdata->getTotalTaxMap() as $item) { $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax(getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); + $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 } @@ -172,36 +174,16 @@ class CreateXInvoice implements ShouldQueue private function getTaxType(string $name): string { - $taxtype = ""; - switch ($name) { - case "ZeroRate": - $taxtype = "Z"; - break; - case "Tax Exempt": - $taxtype = "E"; - break; - case "Reversal of tax liabilty": - $taxtype = "AE"; - break; - case "intra-community delivery": - $taxtype = "K"; - break; - case "Out of EU": - $taxtype = "G"; - break; - case "Outside the tax scope": - $taxtype = "O"; - break; - case "Canary Islands": - $taxtype = "L"; - break; - case "Ceuta / Melila": - $taxtype = "M"; - break; - default: - $taxtype = "S"; - break; - } - return $taxtype; + return match ($name) { + "ZeroRate" => "Z", + "Tax Exempt" => "E", + "Reversal of tax liabilty" => "AE", + "intra-community delivery" => "K", + "Out of EU" => "G", + "Outside the tax scope" => "O", + "Canary Islands" => "L", + "Ceuta / Melila" => "M", + default => "S", + }; } } From cdda4a659472bfe1835be842c3b07281bd2d4608 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 14:33:23 +0200 Subject: [PATCH 27/35] More improvements and fixes --- app/Jobs/Invoice/CreateXInvoice.php | 64 ++++++++++++++++++----------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 8a414bb85038..a64a66a0eea9 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -3,7 +3,6 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; -use App\Models\Country; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; @@ -19,7 +18,7 @@ class CreateXInvoice implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public Invoice $invoice; + private Invoice $invoice; private bool $alterpdf; private string $custompdfpath; @@ -75,6 +74,7 @@ class CreateXInvoice implements ShouldQueue ->setDocumentSupplyChainEvent(date_create($invoice->date)) ->setDocumentSeller($company->getSetting('name')) ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2) + ->setDocumentSellerContact($invoice->user->first_name." ".$invoice->user->last_name) ->setDocumentBuyer($client->name, $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2) ->setDocumentBuyerReference($client->leitweg_id) @@ -94,7 +94,7 @@ class CreateXInvoice implements ShouldQueue } $invoicingdata = $invoice->calc(); - + $globaltax = null; //Create line items and calculate taxes foreach ($invoice->line_items as $index => $item) { @@ -107,26 +107,40 @@ class CreateXInvoice implements ShouldQueue } else { $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); } - - // According to european law, each line item can only have one tax rate - if (!empty($item->tax_name1)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name1), 'VAT', $item->tax_rate1); - } elseif (!empty($item->tax_name2)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); - } elseif (!empty($item->tax_name3)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); - } else { - nlog("Can't add correct tax position"); + $linenetamount = $item->line_total + $item->surcharge_1 + $item->surcharge_2 + $item->surcharge_3; + if ($item->discount > 0){ + if ($invoice->is_amount_discount){ + $linenetamount -= $item->discount; + } + else { + $linenetamount -= $linenetamount * ($item->discount / 100); + } } - - if (!empty($invoice->tax_name1)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); - } elseif (!empty($invoice->tax_name2)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); - } elseif (!empty($invoice->tax_name3)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); + $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_name1), 'VAT', $item->tax_rate1); + } elseif (!empty($item->tax_name2)) { + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); + } elseif (!empty($item->tax_name3)) { + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); + } else { + nlog("Can't add correct tax position"); + } } else { - nlog("Can't add correct tax position"); + if (!empty($invoice->tax_name1)) { + $globaltax = 0; + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name1), 'VAT', $invoice->tax_rate1); + } elseif (!empty($invoice->tax_name2)) { + $globaltax = 1; + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name2), 'VAT', $invoice->tax_rate2); + } elseif (!empty($invoice->tax_name3)) { + $globaltax = 2; + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3), 'VAT', $item->tax_rate3); + } else { + nlog("Can't add correct tax position"); + } } } @@ -142,9 +156,9 @@ class CreateXInvoice implements ShouldQueue $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 } - foreach ($invoicingdata->getTotalTaxMap() as $item) { - $tax = explode(" ", $item["name"]); - $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $item["total"] / (explode("%", end($tax))[0] / 100), $item["total"], explode("%", end($tax))[0]); + if (!empty($globaltax)){ + $tax = explode(" ", $invoicingdata->getTotalTaxMap()[$globaltax]["name"]); + $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $invoicingdata->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicingdata->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); // TODO: Add correct tax type within getTaxType } @@ -153,6 +167,7 @@ class CreateXInvoice implements ShouldQueue Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); } $xrechnung->writeFile(Storage::disk($disk)->path($client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); + // The validity can be checked using https://portal3.gefeg.com/invoice/validation if ($this->alterpdf) { if ($this->custompdfpath != "") { @@ -169,6 +184,7 @@ class CreateXInvoice implements ShouldQueue } } } + return $client->invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); } From 66b0d287b8520fbd15ded3b3d7416be0301fe1ca Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 14:57:26 +0200 Subject: [PATCH 28/35] Added compatibilty to XRechnung 2.2 --- app/Jobs/Invoice/CreateXInvoice.php | 8 ++++++-- lang/en/texts.php | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index a64a66a0eea9..c15268153788 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -74,7 +74,7 @@ class CreateXInvoice implements ShouldQueue ->setDocumentSupplyChainEvent(date_create($invoice->date)) ->setDocumentSeller($company->getSetting('name')) ->setDocumentSellerAddress($company->getSetting("address1"), "", "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2) - ->setDocumentSellerContact($invoice->user->first_name." ".$invoice->user->last_name) + ->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) ->setDocumentBuyerReference($client->leitweg_id) @@ -86,6 +86,10 @@ class CreateXInvoice implements ShouldQueue if (!empty($invoice->po_number)) { $xrechnung->setDocumentBuyerOrderReferencedDocument($invoice->po_number); } + if (empty($client->leitweg_id)){ + $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); + } + $xrechnung->addDocumentPaymentMean(10, ""); if (str_contains($company->getSetting('vat_number'), "/")) { $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); @@ -107,7 +111,7 @@ class CreateXInvoice implements ShouldQueue } else { $xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); } - $linenetamount = $item->line_total + $item->surcharge_1 + $item->surcharge_2 + $item->surcharge_3; + $linenetamount = $item->line_total; if ($item->discount > 0){ if ($invoice->is_amount_discount){ $linenetamount -= $item->discount; diff --git a/lang/en/texts.php b/lang/en/texts.php index 69d2394c31c6..dc2aef973ffb 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5021,7 +5021,7 @@ $LANG = array( 'payment_type_Klarna' => 'Klarna', 'payment_type_Interac E Transfer' => 'Interac E Transfer', 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', - + 'xinvoice_no_buyers_reference' => "No buyer's reference given", ); From 6756de3c2b959ee78557434bd519a379b6f2f769 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 15:31:40 +0200 Subject: [PATCH 29/35] Added Xinvoice to more pdf-downlaods --- app/Jobs/Entity/CreateEntityPdf.php | 8 ++++++-- app/Jobs/Invoice/CreateXInvoice.php | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index 9bdb48f53767..4d8b08939e1d 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -12,6 +12,7 @@ namespace App\Jobs\Entity; use App\Exceptions\FilePermissionsFailure; +use App\Jobs\Invoice\CreateXInvoice; use App\Libraries\MultiDB; use App\Models\Credit; use App\Models\CreditInvitation; @@ -211,7 +212,9 @@ class CreateEntityPdf implements ShouldQueue throw new FilePermissionsFailure($e->getMessage()); } } - + if ($this->entity_string == "invoice" && $this->company->use_xinvoice){ + (new CreateXInvoice($this->entity, true))->handle(); + } $this->invitation = null; $this->entity = null; $this->company = null; @@ -219,7 +222,8 @@ class CreateEntityPdf implements ShouldQueue $this->contact = null; $maker = null; $state = null; - + + return $file_path; } diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index c15268153788..49a47688148d 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -73,12 +73,13 @@ class CreateXInvoice implements ShouldQueue ->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("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2) + ->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) ->setDocumentBuyerReference($client->leitweg_id) ->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); From 5ce5e8f04223b0526dbe984d2fd7321c03c16e4e Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Wed, 5 Apr 2023 15:43:02 +0200 Subject: [PATCH 30/35] Added accurate payment instructions --- app/Jobs/Invoice/CreateXInvoice.php | 2 +- lang/en/texts.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 49a47688148d..1e2ee2c4d07c 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -90,7 +90,7 @@ class CreateXInvoice implements ShouldQueue if (empty($client->leitweg_id)){ $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); } - $xrechnung->addDocumentPaymentMean(10, ""); + $xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); if (str_contains($company->getSetting('vat_number'), "/")) { $xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); diff --git a/lang/en/texts.php b/lang/en/texts.php index dc2aef973ffb..971aa8386cb8 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5022,6 +5022,7 @@ $LANG = array( 'payment_type_Interac E Transfer' => 'Interac E Transfer', 'xinvoice_payable' => 'Payable within :payeddue days net until :paydate', 'xinvoice_no_buyers_reference' => "No buyer's reference given", + 'xinvoice_online_payment' => 'The invoice needs to be payed online via the provided link', ); From 573b802f70f6c0c2585df8eca9a423b5345466db Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Thu, 6 Apr 2023 11:08:07 +0200 Subject: [PATCH 31/35] Fixed a little typo --- app/Jobs/Invoice/CreateXInvoice.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 1e2ee2c4d07c..34353a418b19 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -190,7 +190,7 @@ class CreateXInvoice implements ShouldQueue } } - return $client->invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); + return $client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); } private function getTaxType(string $name): string From 8d057bb10df2aef2b2f688600e0abd4135ce90d5 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Thu, 6 Apr 2023 11:08:59 +0200 Subject: [PATCH 32/35] Added tests for XINvoice --- tests/Unit/XInvoiceTest.php | 61 +++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 tests/Unit/XInvoiceTest.php diff --git a/tests/Unit/XInvoiceTest.php b/tests/Unit/XInvoiceTest.php new file mode 100644 index 000000000000..cfe93649328f --- /dev/null +++ b/tests/Unit/XInvoiceTest.php @@ -0,0 +1,61 @@ +withoutMiddleware( + ThrottleRequests::class + ); + $this->makeTestData(); + } + + public function testXInvoiceGenerates() + { + $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); + $this->assertNotNull($xinvoice); + $this->assertFileExists($xinvoice); + } + + public function testValidityofXMLFile() + { + $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); + $document = ZugferdDocumentReader::readAndGuessFromFile($xinvoice); + $document ->getDocumentInformation($documentno); + $this->assertEquals($this->invoice->number, $documentno); + } + public function checkEmbededPDFFile() + { + $pdf = (new CreateEntityPdf($this->invoice->invitations()->first())); + (new CreateXInvoice($this->invoice, true, $pdf))->handle(); + $document = ZugferdDocumentReader::readAndGuessFromFile($pdf); + $document ->getDocumentInformation($documentno); + $this->assertEquals($this->invoice->number, $documentno); + } +} From 7965f515076158a19a2d9822cccb8d677437c1ab Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Thu, 6 Apr 2023 11:48:32 +0200 Subject: [PATCH 33/35] Added support for the new auto-tax model --- app/Jobs/Invoice/CreateXInvoice.php | 70 ++++++++++++++++++++--------- app/Models/Product.php | 2 + 2 files changed, 52 insertions(+), 20 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 34353a418b19..777f8473f33c 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -3,6 +3,8 @@ namespace App\Jobs\Invoice; use App\Models\Invoice; +use App\Models\Product; +use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdDocumentPdfBuilder; use horstoeko\zugferd\ZugferdProfiles; @@ -125,24 +127,24 @@ class CreateXInvoice implements ShouldQueue // 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_name1), 'VAT', $item->tax_rate1); + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate1); } elseif (!empty($item->tax_name2)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name2), 'VAT', $item->tax_rate2); + $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_id, $invoice), 'VAT', $item->tax_rate2); } elseif (!empty($item->tax_name3)) { - $xrechnung->addDocumentPositionTax($this->getTaxType($item->tax_name3), 'VAT', $item->tax_rate3); + $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), 'VAT', $invoice->tax_rate1); + $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), 'VAT', $invoice->tax_rate2); + $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), 'VAT', $item->tax_rate3); + $xrechnung->addDocumentPositionTax($this->getTaxType($invoice->tax_name3, $invoice), 'VAT', $item->tax_rate3); } else { nlog("Can't add correct tax position"); } @@ -158,12 +160,12 @@ class CreateXInvoice implements ShouldQueue foreach ($invoicingdata->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]); + $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)){ $tax = explode(" ", $invoicingdata->getTotalTaxMap()[$globaltax]["name"]); - $xrechnung->addDocumentTax($this->getTaxType(""), "VAT", $invoicingdata->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicingdata->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); + $xrechnung->addDocumentTax($this->getTaxType("", $invoice), "VAT", $invoicingdata->getTotalTaxMap()[$globaltax]["total"] / (explode("%", end($tax))[0] / 100), $invoicingdata->getTotalTaxMap()[$globaltax]["total"], explode("%", end($tax))[0]); // TODO: Add correct tax type within getTaxType } @@ -193,18 +195,46 @@ class CreateXInvoice implements ShouldQueue return $client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); } - private function getTaxType(string $name): string + private function getTaxType($name, Invoice $invoice): string { - return match ($name) { - "ZeroRate" => "Z", - "Tax Exempt" => "E", - "Reversal of tax liabilty" => "AE", - "intra-community delivery" => "K", - "Out of EU" => "G", - "Outside the tax scope" => "O", - "Canary Islands" => "L", - "Ceuta / Melila" => "M", - default => "S", - }; + $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/Models/Product.php b/app/Models/Product.php index e360fd145ba7..9c6dfaee5b60 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -121,6 +121,8 @@ class Product extends BaseModel public const PRODUCT_TYPE_EXEMPT = 5; public const PRODUCT_TYPE_REDUCED_TAX = 6; public const PRODUCT_TYPE_OVERRIDE_TAX = 7; + public const PRODUCT_TYPE_ZERO_RATED = 8; + public const PRODUCT_TYPE_REVERSE_TAX = 9; protected $fillable = [ 'custom_value1', From 24b299319846265b682c73fc7112c7907e6eb844 Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Thu, 6 Apr 2023 15:54:08 +0200 Subject: [PATCH 34/35] Corrected minor issue --- app/Jobs/Invoice/CreateXInvoice.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 777f8473f33c..1a59ff84ec90 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -153,9 +153,9 @@ class CreateXInvoice implements ShouldQueue if ($invoice->isPartial()) { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount - $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial); + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, $invoice->partial); } else { - $xrechnung->setDocumentSummation($invoice->amount, $invoice->amount - $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); + $xrechnung->setDocumentSummation($invoice->amount, $invoice->balance, $invoicingdata->getSubTotal(), $invoicingdata->getTotalSurcharges(), $invoicingdata->getTotalDiscount(), $invoicingdata->getSubTotal(), $invoicingdata->getItemTotalTaxes(), null, 0.0); } foreach ($invoicingdata->getTaxMap() as $item) { From 81b5c8783e52cb2b73b6233ec4e12b8c7c2c4a1f Mon Sep 17 00:00:00 2001 From: Lars Kusch Date: Mon, 17 Apr 2023 09:24:16 +0200 Subject: [PATCH 35/35] Renamed to e_invoice --- app/Console/Commands/SendRemindersCron.php | 4 ++-- app/Http/Controllers/InvoiceController.php | 4 ++-- app/Jobs/Entity/CreateEntityPdf.php | 2 +- app/Jobs/Invoice/CreateXInvoice.php | 14 +++++++------- app/Models/Client.php | 10 +++++----- app/Models/Company.php | 10 +++++----- app/Services/Email/EmailDefaults.php | 10 +++++----- app/Services/Invoice/GetInvoiceXInvoice.php | 2 +- app/Services/Invoice/InvoiceService.php | 12 ++++++------ ...156872_add_e_invoice_type_to_clients_table.php} | 6 +++--- routes/api.php | 2 +- routes/client.php | 2 +- tests/Unit/{XInvoiceTest.php => EInvoiceTest.php} | 13 ++++++++++--- 13 files changed, 49 insertions(+), 42 deletions(-) rename database/migrations/{2023_03_13_156872_add_leitweg_id_to_clients_table.php => 2023_03_13_156872_add_e_invoice_type_to_clients_table.php} (73%) rename tests/Unit/{XInvoiceTest.php => EInvoiceTest.php} (89%) diff --git a/app/Console/Commands/SendRemindersCron.php b/app/Console/Commands/SendRemindersCron.php index 68da45d79e21..7921633c8d9d 100644 --- a/app/Console/Commands/SendRemindersCron.php +++ b/app/Console/Commands/SendRemindersCron.php @@ -175,8 +175,8 @@ class SendRemindersCron extends Command $invoice->calc()->getInvoice()->save(); $invoice->fresh(); $invoice->service()->deletePdf()->save(); - if ($invoice->company->use_xinvoice){ - $invoice->service()->deleteXInvoice()->save(); + if ($invoice->company->enable_e_invoice){ + $invoice->service()->deleteEInvoice()->save(); } /* Refresh the client here to ensure the balance is fresh */ diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 479e647a7f32..1d7ddc21d76e 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -903,7 +903,7 @@ class InvoiceController extends BaseController * @param $invitation_key * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ - public function downloadXInvoice($invitation_key) + public function downloadEInvoice($invitation_key) { $invitation = $this->invoice_repo->getInvitationByKey($invitation_key); @@ -914,7 +914,7 @@ class InvoiceController extends BaseController $contact = $invitation->contact; $invoice = $invitation->invoice; - $file = $invoice->service()->getXInvoice($contact); + $file = $invoice->service()->getEInvoice($contact); $headers = ['Content-Type' => 'application/xml']; diff --git a/app/Jobs/Entity/CreateEntityPdf.php b/app/Jobs/Entity/CreateEntityPdf.php index 4d8b08939e1d..631bf2d35ef5 100644 --- a/app/Jobs/Entity/CreateEntityPdf.php +++ b/app/Jobs/Entity/CreateEntityPdf.php @@ -212,7 +212,7 @@ class CreateEntityPdf implements ShouldQueue throw new FilePermissionsFailure($e->getMessage()); } } - if ($this->entity_string == "invoice" && $this->company->use_xinvoice){ + if ($this->entity_string == "invoice" && $this->company->enable_e_invoice){ (new CreateXInvoice($this->entity, true))->handle(); } $this->invitation = null; diff --git a/app/Jobs/Invoice/CreateXInvoice.php b/app/Jobs/Invoice/CreateXInvoice.php index 1a59ff84ec90..833e85c4fa93 100644 --- a/app/Jobs/Invoice/CreateXInvoice.php +++ b/app/Jobs/Invoice/CreateXInvoice.php @@ -43,7 +43,7 @@ class CreateXInvoice implements ShouldQueue $company = $invoice->company; $client = $invoice->client; $profile = ""; - switch ($company->xinvoice_type) { + switch ($company->e_invoice_type) { case "EN16931": $profile = ZugferdProfiles::PROFILE_EN16931; break; @@ -79,7 +79,7 @@ class CreateXInvoice implements ShouldQueue ->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) - ->setDocumentBuyerReference($client->leitweg_id) + ->setDocumentBuyerReference($client->routing_id) ->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])); @@ -89,7 +89,7 @@ class CreateXInvoice implements ShouldQueue if (!empty($invoice->po_number)) { $xrechnung->setDocumentBuyerOrderReferencedDocument($invoice->po_number); } - if (empty($client->leitweg_id)){ + if (empty($client->routing_id)){ $xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); } $xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); @@ -170,10 +170,10 @@ class CreateXInvoice implements ShouldQueue } $disk = config('filesystems.default'); - if (!Storage::exists($client->xinvoice_filepath($invoice->invitations->first()))) { - Storage::makeDirectory($client->xinvoice_filepath($invoice->invitations->first())); + 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->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"))); + $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) { @@ -192,7 +192,7 @@ class CreateXInvoice implements ShouldQueue } } - return $client->xinvoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); + return $client->e_invoice_filepath($invoice->invitations->first()) . $invoice->getFileName("xml"); } private function getTaxType($name, Invoice $invoice): string diff --git a/app/Models/Client.php b/app/Models/Client.php index 09614ef95067..e18e4e92e25e 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -41,7 +41,7 @@ use Laracasts\Presenter\PresentableTrait; * @property string|null $client_hash * @property string|null $logo * @property string|null $phone - * @property string|null leitweg_id + * @property string|null routing_id * @property string $balance * @property string $paid_to_date * @property string $credit_balance @@ -363,7 +363,7 @@ class Client extends BaseModel implements HasLocalePreference 'public_notes', 'phone', 'number', - 'leitweg_id', + 'routing_id', ]; protected $with = [ @@ -413,7 +413,7 @@ class Client extends BaseModel implements HasLocalePreference 'id_number', 'public_notes', 'phone', - 'leitweg_id', + 'routing_id', ]; // public function scopeExclude($query) @@ -875,11 +875,11 @@ class Client extends BaseModel implements HasLocalePreference return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/'; } - public function xinvoice_filepath($invitation) + public function e_invoice_filepath($invitation) { $contact_key = $invitation->contact->contact_key; - return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/xinvoice/'; + return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/e_invoice/'; } public function quote_filepath($invitation) diff --git a/app/Models/Company.php b/app/Models/Company.php index 8dd4f0cc43b1..fca440706956 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -97,8 +97,8 @@ use Laracasts\Presenter\PresentableTrait; * @property int $stock_notification * @property string|null $matomo_url * @property int|null $matomo_id - * @property bool $use_xinvoice - * @property string $xinvoice_type + * @property bool $enable_e_invoice + * @property string $e_invoice_type * @property int $enabled_expense_tax_rates * @property int $invoice_task_project * @property int $report_include_deleted @@ -746,8 +746,8 @@ class Company extends BaseModel 'google_analytics_key', 'matomo_url', 'matomo_id', - 'use_xinvoice', - 'xinvoice_type', + 'enable_e_invoice', + 'e_invoice_type', 'client_can_register', 'enable_shop_api', 'invoice_task_timelog', @@ -825,7 +825,7 @@ class Company extends BaseModel public function refreshTaxData() { - + } public function documents() diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 3994dc82574a..13b8294bb146 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -267,7 +267,7 @@ class EmailDefaults return $this; // return $this->email->email_object->cc; // return [ - + // ]; } @@ -298,7 +298,7 @@ class EmailDefaults $this->email->email_object->entity instanceof Quote || $this->email->email_object->entity instanceof Credit)) { $pdf = ((new CreateRawPdf($this->email->email_object->invitation, $this->email->company->db))->handle()); - if ($this->email->email_object->company->use_xinvoice && $this->email->email_object->entity instanceof Invoice) { + 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(); @@ -319,10 +319,10 @@ class EmailDefaults $this->email->email_object->attachments = array_merge($this->email->email_object->attachments, [['file' => base64_encode($ubl_string), 'name' => $this->email->email_object->entity->getFileName('xml')]]); } } - /** XInvoice xml file */ - if ($this->email->email_object->company->use_xinvoice && $this->email->email_object->entity instanceof Invoice) { + /** E-Invoice xml file */ + if ($this->email->email_object->company->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) { $xinvoice_path = (new GetInvoiceXInvoice($this->email->email_object->entity))->run(); - $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"]]); + $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]."-e_invoice.xml"]]); } if (!$this->email->email_object->settings->document_email_attachment || !$this->email->company->account->hasFeature(Account::FEATURE_DOCUMENTS)) { diff --git a/app/Services/Invoice/GetInvoiceXInvoice.php b/app/Services/Invoice/GetInvoiceXInvoice.php index f5d279d34fb3..ebe07aca1530 100644 --- a/app/Services/Invoice/GetInvoiceXInvoice.php +++ b/app/Services/Invoice/GetInvoiceXInvoice.php @@ -38,7 +38,7 @@ class GetInvoiceXInvoice extends AbstractService $invitation = $this->invoice->invitations->first(); } - $file_path = $this->invoice->client->xinvoice_filepath($this->invoice->invitations->first()). $this->invoice->getFileName("xml"); + $file_path = $this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()). $this->invoice->getFileName("xml"); // $disk = 'public'; $disk = config('filesystems.default'); diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 8b180ef12c36..e7bf0d28375d 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -186,7 +186,7 @@ class InvoiceService return (new GenerateDeliveryNote($invoice, $contact))->run(); } - public function getXInvoice($contact = null) + public function getEInvoice($contact = null) { return (new GetInvoiceXInvoice($this->invoice, $contact))->run(); } @@ -358,18 +358,18 @@ class InvoiceService return $this; } - public function deleteXInvoice() + public function deleteEInvoice() { $this->invoice->load('invitations'); $this->invoice->invitations->each(function ($invitation) { try { - if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->xinvoice_filepath($invitation).$this->invoice->getFileName("xml"))) { - Storage::disk(config('filesystems.default'))->delete($this->invoice->client->xinvoice_filepath($invitation).$this->invoice->getFileName("xml")); + if (Storage::disk(config('filesystems.default'))->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + Storage::disk(config('filesystems.default'))->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml")); } - if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { - Storage::disk('public')->delete($this->invoice->client->invoice_filepath($invitation).$this->invoice->getFileName("xml")); + if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + Storage::disk('public')->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml")); } } catch (\Exception $e) { nlog($e->getMessage()); diff --git a/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php b/database/migrations/2023_03_13_156872_add_e_invoice_type_to_clients_table.php similarity index 73% rename from database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php rename to database/migrations/2023_03_13_156872_add_e_invoice_type_to_clients_table.php index 761cb6404fa5..ad5129219784 100644 --- a/database/migrations/2023_03_13_156872_add_leitweg_id_to_clients_table.php +++ b/database/migrations/2023_03_13_156872_add_e_invoice_type_to_clients_table.php @@ -15,11 +15,11 @@ return new class extends Migration { Schema::table('clients', function (Blueprint $table) { - $table->string('leitweg_id')->default(null); + $table->string('routing_id')->default(null)->nullable(); }); Schema::table('companies', function (Blueprint $table) { - $table->boolean('use_xinvoice')->default(false); - $table->string('xinvoice_type')->default("EN16931"); + $table->boolean('enable_e_invoice')->default(false); + $table->string('e_invoice_type')->default("EN16931"); }); } diff --git a/routes/api.php b/routes/api.php index 40876ae2389c..88cf3ec12be9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -205,7 +205,7 @@ Route::group(['middleware' => ['throttle:api', 'api_db', 'token_auth', 'locale'] Route::get('invoices/{invoice}/{action}', [InvoiceController::class, 'action'])->name('invoices.action'); Route::put('invoices/{invoice}/upload', [InvoiceController::class, 'upload'])->name('invoices.upload'); Route::get('invoice/{invitation_key}/download', [InvoiceController::class, 'downloadPdf'])->name('invoices.downloadPdf'); - Route::get('invoice/{invitation_key}/download_xinvoice', [InvoiceController::class, 'downloadXInvoice_pdf'])->name('invoices.downloadXInvoice'); + Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoices.downloadEInvoice'); Route::post('invoices/bulk', [InvoiceController::class, 'bulk'])->name('invoices.bulk'); Route::post('invoices/update_reminders', [InvoiceController::class, 'update_reminders'])->name('invoices.update_reminders'); diff --git a/routes/client.php b/routes/client.php index 2c7d700947da..4c55285c8fce 100644 --- a/routes/client.php +++ b/routes/client.php @@ -128,7 +128,7 @@ Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'clie Route::get('credit/{invitation_key}', [App\Http\Controllers\ClientPortal\InvitationController::class, 'creditRouter']); Route::get('recurring_invoice/{invitation_key}/download_pdf', [RecurringInvoiceController::class, 'downloadPdf'])->name('recurring_invoice.download_invitation_key'); Route::get('invoice/{invitation_key}/download_pdf', [InvoiceController::class, 'downloadPdf'])->name('invoice.download_invitation_key'); - Route::get('invoice/{invitation_key}/download_xinvoice', [InvoiceController::class, 'downloadXInvoice'])->name('invoice.download_xinvoice'); + Route::get('invoice/{invitation_key}/download_e_invoice', [InvoiceController::class, 'downloadEInvoice'])->name('invoice.download_e_invoice'); Route::get('quote/{invitation_key}/download_pdf', [QuoteController::class, 'downloadPdf'])->name('quote.download_invitation_key'); Route::get('credit/{invitation_key}/download_pdf', [CreditController::class, 'downloadPdf'])->name('credit.download_invitation_key'); Route::get('{entity}/{invitation_key}/download', [App\Http\Controllers\ClientPortal\InvitationController::class, 'routerForDownload']); diff --git a/tests/Unit/XInvoiceTest.php b/tests/Unit/EInvoiceTest.php similarity index 89% rename from tests/Unit/XInvoiceTest.php rename to tests/Unit/EInvoiceTest.php index cfe93649328f..a67661977efb 100644 --- a/tests/Unit/XInvoiceTest.php +++ b/tests/Unit/EInvoiceTest.php @@ -19,9 +19,9 @@ use horstoeko\zugferd\ZugferdDocumentReader; /** * @test - * @covers App\Jobs\Invoice\CreateUbl + * @covers App\Jobs\Invoice\CreateXInvoice */ -class XInvoiceTest extends TestCase +class EInvoiceTest extends TestCase { use MockAccountData; use DatabaseTransactions; @@ -36,13 +36,16 @@ class XInvoiceTest extends TestCase $this->makeTestData(); } - public function testXInvoiceGenerates() + public function testEInvoiceGenerates() { $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); $this->assertNotNull($xinvoice); $this->assertFileExists($xinvoice); } + /** + * @throws Exception + */ public function testValidityofXMLFile() { $xinvoice = (new CreateXInvoice($this->invoice, false))->handle(); @@ -50,6 +53,10 @@ class XInvoiceTest extends TestCase $document ->getDocumentInformation($documentno); $this->assertEquals($this->invoice->number, $documentno); } + + /** + * @throws Exception + */ public function checkEmbededPDFFile() { $pdf = (new CreateEntityPdf($this->invoice->invitations()->first()));