diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index 3eedd8fe2ffc..e0db0722c133 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -727,6 +727,74 @@ class CreditController extends BaseController }, $credit->numberFormatter() . '.pdf', $headers); } + /** + * @OA\Get( + * path="/api/v1/credit/{invitation_key}/download_e_credit", + * operationId="downloadXcredit", + * tags={"credit"}, + * summary="Download a specific x-credit by invitation key", + * description="Downloads a specific x-credit", + * @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 credit Invitation Key", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the x-credit 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 downloadECredit($invitation_key) + { + $invitation = $this->credit_repository->getInvitationByKey($invitation_key); + + if (! $invitation) { + return response()->json(['message' => 'no record found'], 400); + } + + $contact = $invitation->contact; + $credit = $invitation->credit; + + $file = $credit->service()->getEInvoice($contact); + $file_name = $credit->getFileName("xml"); + + $headers = ['Content-Type' => 'application/xml']; + + if (request()->input('inline') == 'true') { + $headers = array_merge($headers, ['Content-Disposition' => 'inline']); + } + + return response()->streamDownload(function () use ($file) { + echo $file; + }, $file_name, $headers); + } + /** * Update the specified resource in storage. diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 12b8cb143da5..1682380e02f1 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -851,4 +851,71 @@ class PurchaseOrderController extends BaseController echo $file; }, $purchase_order->numberFormatter().".pdf", $headers); } + /** + * @OA\Get( + * path="/api/v1/credit/{invitation_key}/download_e_purchase_order", + * operationId="downloadEPurchaseOrder", + * tags={"purchase_orders"}, + * summary="Download a specific E-Purchase-Order by invitation key", + * description="Downloads a specific E-Purchase-Order", + * @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 E-Purchase-Order Invitation Key", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the E-Purchase-Order 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 downloadEPurchaseOrder($invitation_key) + { + $invitation = $this->purchase_order_repository->getInvitationByKey($invitation_key); + + if (! $invitation) { + return response()->json(['message' => 'no record found'], 400); + } + + $contact = $invitation->contact; + $purchase_order = $invitation->purchase_order; + + $file = $purchase_order->service()->getEPurchaseOrder($contact); + $file_name = $purchase_order->getFileName("xml"); + + $headers = ['Content-Type' => 'application/xml']; + + if (request()->input('inline') == 'true') { + $headers = array_merge($headers, ['Content-Disposition' => 'inline']); + } + + return response()->streamDownload(function () use ($file) { + echo $file; + }, $file_name, $headers); + } } diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 78a5bf625247..1ac67895f009 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -860,6 +860,75 @@ class QuoteController extends BaseController } + /** + * @OA\Get( + * path="/api/v1/invoice/{invitation_key}/download_e_quote", + * operationId="downloadXQuote", + * tags={"quotes"}, + * summary="Download a specific x-quote by invitation key", + * description="Downloads a specific x-quote", + * @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 Quote Invitation Key", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the x-quote 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 downloadEQuote($invitation_key) + { + $invitation = $this->quote_repo->getInvitationByKey($invitation_key); + + if (! $invitation) { + return response()->json(['message' => 'no record found'], 400); + } + + $contact = $invitation->contact; + $quote = $invitation->quote; + + $file = $quote->service()->getEInvoice($contact); + $file_name = $quote->getFileName("xml"); + + $headers = ['Content-Type' => 'application/xml']; + + if (request()->input('inline') == 'true') { + $headers = array_merge($headers, ['Content-Disposition' => 'inline']); + } + + return response()->streamDownload(function () use ($file) { + echo $file; + }, $file_name, $headers); + } + + /** * Update the specified resource in storage. * diff --git a/app/Jobs/EDocument/CreateEDocument.php b/app/Jobs/EDocument/CreateEDocument.php new file mode 100644 index 000000000000..59226608316c --- /dev/null +++ b/app/Jobs/EDocument/CreateEDocument.php @@ -0,0 +1,139 @@ +document instanceof PurchaseOrder) ? $this->document->vendor : $this->document->client; + App::setLocale($$settings_entity->locale()); + + /* Set customized translations _NOW_ */ + $t->replace(Ninja::transformTranslations($this->document->client->getMergedSettings())); + + $e_document_type = $settings_entity->getSetting('e_invoice_type') ? $settings_entity->getSetting('e_invoice_type') : "XInvoice_3_0"; + $e_quote_type = $settings_entity->getSetting('e_quote_type') ? $settings_entity->getSetting('e_quote_type') : "OrderX_Extended"; + if ($this->document instanceof Invoice){ + switch ($e_document_type) { + case "EN16931": + case "XInvoice_3_0": + case "XInvoice_2_3": + case "XInvoice_2_2": + case "XInvoice_2_1": + case "XInvoice_2_0": + case "XInvoice_1_0": + case "XInvoice-Extended": + case "XInvoice-BasicWL": + case "XInvoice-Basic": + $zugferd = (new ZugferdEDokument($this->document))->run(); + + return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml(); + case "Facturae_3.2": + case "Facturae_3.2.1": + case "Facturae_3.2.2": + return (new FacturaEInvoice($this->document, str_replace("Facturae_", "", $e_document_type)))->run(); + default: + + $zugferd = (new ZugferdEDokument($this->document))->run(); + + return $this->returnObject ? $zugferd : $zugferd->getXml(); + + } + } + elseif ($this->document instanceof Quote){ + switch ($e_quote_type){ + case "OrderX_Basic": + case "OrderX_Comfort": + case "OrderX_Extended": + $orderx = (new OrderXDocument($this->document))->run(); + return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml(); + default: + $orderx = (new OrderXDocument($this->document))->run(); + return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml(); + } + } + elseif ($this->document instanceof PurchaseOrder){ + switch ($e_quote_type){ + case "OrderX_Basic": + case "OrderX_Comfort": + case "OrderX_Extended": + $orderx = (new OrderXDocument($this->document))->run(); + return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml(); + default: + $orderx = (new OrderXDocument($this->document))->run(); + return $this->returnObject ? $orderx->orderxdocument : $orderx->getXml(); + } + } + elseif ($this->document instanceof Credit) { + switch ($e_document_type) { + case "EN16931": + case "XInvoice_3_0": + case "XInvoice_2_3": + case "XInvoice_2_2": + case "XInvoice_2_1": + case "XInvoice_2_0": + case "XInvoice_1_0": + case "XInvoice-Extended": + case "XInvoice-BasicWL": + case "XInvoice-Basic": + $zugferd = (new ZugferdEDokument($this->document))->run(); + return $this->returnObject ? $zugferd->xdocument : $zugferd->getXml(); + default: + $zugferd = (new ZugferdEDokument($this->document))->run(); + return $this->returnObject ? $zugferd : $zugferd->getXml(); + } + } + else{ + return ""; + } + } +} diff --git a/app/Jobs/Invoice/CreateEInvoice.php b/app/Jobs/Invoice/CreateEInvoice.php deleted file mode 100644 index 0ea7ae8a96d8..000000000000 --- a/app/Jobs/Invoice/CreateEInvoice.php +++ /dev/null @@ -1,86 +0,0 @@ -invoice->client->locale()); - - /* Set customized translations _NOW_ */ - $t->replace(Ninja::transformTranslations($this->invoice->client->getMergedSettings())); - - $e_invoice_type = $this->invoice->client->getSetting('e_invoice_type'); - - switch ($e_invoice_type) { - case "EN16931": - case "XInvoice_3_0": - case "XInvoice_2_3": - case "XInvoice_2_2": - case "XInvoice_2_1": - case "XInvoice_2_0": - case "XInvoice_1_0": - case "XInvoice-Extended": - case "XInvoice-BasicWL": - case "XInvoice-Basic": - $zugferd = (new ZugferdEInvoice($this->invoice))->run(); - - return $this->returnObject ? $zugferd->xrechnung : $zugferd->getXml(); - case "Facturae_3.2": - case "Facturae_3.2.1": - case "Facturae_3.2.2": - return (new FacturaEInvoice($this->invoice, str_replace("Facturae_", "", $e_invoice_type)))->run(); - default: - - $zugferd = (new ZugferdEInvoice($this->invoice))->run(); - - return $this->returnObject ? $zugferd : $zugferd->getXml(); - - } - - } -} diff --git a/app/Jobs/PurchaseOrder/ZipPurchaseOrders.php b/app/Jobs/PurchaseOrder/ZipPurchaseOrders.php index 76a91d6beed3..401e2e5f48e5 100644 --- a/app/Jobs/PurchaseOrder/ZipPurchaseOrders.php +++ b/app/Jobs/PurchaseOrder/ZipPurchaseOrders.php @@ -67,6 +67,11 @@ class ZipPurchaseOrders implements ShouldQueue try { foreach ($invitations as $invitation) { + if ($invitation->purchase_order->vendor->getSetting("enable_e_invoice")) { + $xml = $invitation->purchase_order->service()->getEInvoice(); + $zipFile->addFromString($invitation->purchase_order->getFileName("xml"), $xml); + } + $file = (new CreateRawPdf($invitation))->handle(); $zipFile->addFromString($invitation->purchase_order->numberFormatter().".pdf", $file); diff --git a/app/Jobs/Quote/ZipQuotes.php b/app/Jobs/Quote/ZipQuotes.php index a39c7615a4ce..fad437718993 100644 --- a/app/Jobs/Quote/ZipQuotes.php +++ b/app/Jobs/Quote/ZipQuotes.php @@ -63,6 +63,10 @@ class ZipQuotes implements ShouldQueue try { foreach ($invitations as $invitation) { + if ($invitation->quote->client->getSetting('enable_e_invoice')) { + $xml = $invitation->quote->service()->getEInvoice(); + $zipFile->addFromString($invitation->quote->getFileName("xml"), $xml); + } $file = (new \App\Jobs\Entity\CreateRawPdf($invitation))->handle(); $zipFile->addFromString($invitation->quote->numberFormatter() . '.pdf', $file); } diff --git a/app/Livewire/PdfSlot.php b/app/Livewire/PdfSlot.php index 1322d6a9664f..81ad1fab61c1 100644 --- a/app/Livewire/PdfSlot.php +++ b/app/Livewire/PdfSlot.php @@ -12,7 +12,7 @@ namespace App\Livewire; -use App\Jobs\Invoice\CreateEInvoice; +use App\Jobs\EDocument\CreateEDocument; use App\Libraries\MultiDB; use App\Models\CreditInvitation; use App\Models\InvoiceInvitation; @@ -113,7 +113,7 @@ class PdfSlot extends Component $file_name = $this->entity->numberFormatter().'.xml'; - $file = (new CreateEInvoice($this->entity))->handle(); + $file = (new CreateEDocument($this->entity))->handle(); $headers = ['Content-Type' => 'application/xml']; diff --git a/app/Mail/TemplateEmail.php b/app/Mail/TemplateEmail.php index 9beac3dbe3b2..0272cc925c4c 100644 --- a/app/Mail/TemplateEmail.php +++ b/app/Mail/TemplateEmail.php @@ -159,15 +159,46 @@ class TemplateEmail extends Mailable } } - if ($this->invitation && $this->invitation->invoice && $this->invitation->invoice->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { - $xml_string = $this->invitation->invoice->service()->getEInvoice($this->invitation->contact); + if ($this->invitation->invoice) { + if ($this->invitation && $this->invitation->invoice && $this->invitation->invoice->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + $xml_string = $this->invitation->invoice->service()->getEInvoice($this->invitation->contact); + + if ($xml_string) { + $this->attachData($xml_string, $this->invitation->invoice->getEFileName("xml")); + } - if($xml_string) { - $this->attachData($xml_string, $this->invitation->invoice->getEFileName("xml")); } - } + elseif ($this->invitation->credit){ + if ($this->invitation && $this->invitation->credit && $this->invitation->credit->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + $xml_string = $this->invitation->credit->service()->getECredit($this->invitation->contact); + if ($xml_string) { + $this->attachData($xml_string, $this->invitation->credit->getEFileName("xml")); + } + + } + } + elseif ($this->invitation->quote){ + if ($this->invitation && $this->invitation->quote && $this->invitation->quote->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + $xml_string = $this->invitation->quote->service()->getEQuote($this->invitation->contact); + + if ($xml_string) { + $this->attachData($xml_string, $this->invitation->quote->getEFileName("xml")); + } + + } + } + elseif ($this->invitation->purchase_order){ + if ($this->invitation && $this->invitation->purchase_order && $this->invitation->purchase_order->client->getSetting('enable_e_invoice') && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) { + $xml_string = $this->invitation->purchase_order->service()->getEPurchaseOrder($this->invitation->contact); + + if ($xml_string) { + $this->attachData($xml_string, $this->invitation->purchase_order->getEFileName("xml")); + } + + } + } return $this; } } diff --git a/app/Models/Client.php b/app/Models/Client.php index 6ac021a4bc43..8525cde2784f 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -754,7 +754,7 @@ class Client extends BaseModel implements HasLocalePreference return $this->company->company_key.'/'.$this->client_hash.'/'.$contact_key.'/invoices/'; } - public function e_invoice_filepath($invitation): string + public function e_document_filepath($invitation): string { $contact_key = $invitation->contact->contact_key; diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index deb2f3dffe0b..77409f913e37 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -12,6 +12,7 @@ namespace App\Services\Credit; use App\Factory\PaymentFactory; +use App\Jobs\EDocument\CreateEDocument; use App\Models\Credit; use App\Models\Payment; use App\Models\PaymentType; @@ -37,6 +38,11 @@ class CreditService return (new GetCreditPdf($invitation))->run(); } + public function getECredit($contact = null) + { + return (new CreateEDocument($this->credit))->handle(); + } + /** * Applies the invoice number. * @return $this InvoiceService object @@ -232,6 +238,27 @@ class CreditService return $this; } + public function deleteECredit() + { + $this->credit->load('invitations'); + + $this->credit->invitations->each(function ($invitation) { + try { + // 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->credit->client->e_document_filepath($invitation).$this->credit->getFileName("xml")); + // } + + // if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + if (Ninja::isHosted()) { + Storage::disk('public')->delete($this->credit->client->e_document_filepath($invitation).$this->credit->getFileName("xml")); + } + } catch (\Exception $e) { + nlog($e->getMessage()); + } + }); + + return $this; + } public function triggeredActions($request) { $this->credit = (new TriggeredActions($this->credit, $request))->run(); diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/EDocument/Standards/FacturaEInvoice.php similarity index 99% rename from app/Services/Invoice/EInvoice/FacturaEInvoice.php rename to app/Services/EDocument/Standards/FacturaEInvoice.php index e4b7f28bd905..623fe724d172 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/EDocument/Standards/FacturaEInvoice.php @@ -9,7 +9,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Services\Invoice\EInvoice; +namespace App\Services\EInvoicing\Standards; use App\Models\Invoice; use App\Models\PaymentType; diff --git a/app/Services/Invoice/EInvoice/FatturaPA.php b/app/Services/EDocument/Standards/FatturaPA.php similarity index 99% rename from app/Services/Invoice/EInvoice/FatturaPA.php rename to app/Services/EDocument/Standards/FatturaPA.php index 2c448c7cfa1b..7b40e9c8b33f 100644 --- a/app/Services/Invoice/EInvoice/FatturaPA.php +++ b/app/Services/EDocument/Standards/FatturaPA.php @@ -9,7 +9,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Services\Invoice\EInvoice; +namespace App\Services\EInvoicing\Standards; use App\Models\Invoice; use App\Services\AbstractService; diff --git a/app/Services/EDocument/Standards/OrderXDocument.php b/app/Services/EDocument/Standards/OrderXDocument.php new file mode 100644 index 000000000000..71fe18844344 --- /dev/null +++ b/app/Services/EDocument/Standards/OrderXDocument.php @@ -0,0 +1,250 @@ +document->company; + $settings_entity = ($this->document instanceof PurchaseOrder) ? $this->document->vendor : $this->document->client; + $profile = $settings_entity->getSetting('e_quote_type') ? $settings_entity->getSetting('e_quote_type') : "OrderX_Extended"; + + $profile = match ($profile) { + "OrderX_Basic" => OrderProfiles::PROFILE_BASIC, + "OrderX_Comfort" => OrderProfiles::PROFILE_COMFORT, + "OrderX_Extended" => OrderProfiles::PROFILE_EXTENDED, + default => OrderProfiles::PROFILE_EXTENDED, + }; + + $this->orderxdocument = OrderDocumentBuilder::CreateNew($profile); + + $this->orderxdocument + ->setDocumentSeller($company->getSetting('name')) + ->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state")) + ->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $this->document->user->present()->phone(), "", $this->document->user->email) + ->setDocumentBuyer($settings_entity->present()->name(), $settings_entity->number) + ->setDocumentBuyerAddress($settings_entity->address1, "", "", $settings_entity->postal_code, $settings_entity->city, $settings_entity->country->iso_3166_2, $settings_entity->state) + ->setDocumentBuyerContact($settings_entity->present()->primary_contact_name(), "", $settings_entity->present()->phone(), "", $settings_entity->present()->email()) + ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->document->date ?? now()->format('Y-m-d'))->diff(date_create($this->document->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->document->due_date])); + + if (!empty($this->document->public_notes)) { + $this->orderxdocument->addDocumentNote($this->document->public_notes ?? ''); + } + // Document type + $document_class = get_class($this->document); + switch ($document_class){ + case Quote::class: + // Probably wrong file code https://github.com/horstoeko/zugferd/blob/master/src/codelists/ZugferdInvoiceType.php + if (empty($this->document->number)) { + $this->orderxdocument->setDocumentInformation("DRAFT", OrderDocumentTypes::ORDER, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode()); + $this->orderxdocument->setIsTestDocument(true); + } else { + $this->orderxdocument->setDocumentInformation($this->document->number, OrderDocumentTypes::ORDER, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode()); + }; + break; + case PurchaseOrder::class: + if (empty($this->document->number)) { + $this->orderxdocument->setDocumentInformation("DRAFT", OrderDocumentTypes::ORDER_RESPONSE, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode()); + $this->orderxdocument->setIsTestDocument(true); + } else { + $this->orderxdocument->setDocumentInformation($this->document->number, OrderDocumentTypes::ORDER_RESPONSE, date_create($this->document->date ?? now()->format('Y-m-d')), $settings_entity->getCurrencyCode()); + } + break; + } + if (isset($this->document->po_number)) { + $this->orderxdocument->setDocumentBuyerOrderReferencedDocument($this->document->po_number); + } + + if (empty($settings_entity->routing_id)) { + $this->orderxdocument->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); + } else { + $this->orderxdocument->setDocumentBuyerReference($settings_entity->routing_id); + } + if (isset($settings_entity->shipping_address1) && $settings_entity->shipping_country) { + $this->orderxdocument->setDocumentShipToAddress($settings_entity->shipping_address1, $settings_entity->shipping_address2, "", $settings_entity->shipping_postal_code, $settings_entity->shipping_city, $settings_entity->shipping_country->iso_3166_2, $settings_entity->shipping_state); + } + + $this->orderxdocument->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); + + if (str_contains($company->getSetting('vat_number'), "/")) { + $this->orderxdocument->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); + } else { + $this->orderxdocument->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); + } + + $invoicing_data = $this->document->calc(); + + //Create line items and calculate taxes + foreach ($this->document->line_items as $index => $item) { + /** @var \App\DataMapper\InvoiceItem $item **/ + $this->orderxdocument->addNewPosition($index) + ->setDocumentPositionGrossPrice($item->gross_line_total) + ->setDocumentPositionNetPrice($item->line_total); + if (!empty($item->product_key)) { + if (!empty($item->notes)) { + $this->orderxdocument->setDocumentPositionProductDetails($item->product_key, $item->notes); + } else { + $this->orderxdocument->setDocumentPositionProductDetails($item->product_key); + } + } else { + if (!empty($item->notes)) { + $this->orderxdocument->setDocumentPositionProductDetails($item->notes); + } else { + $this->orderxdocument->setDocumentPositionProductDetails("no product name defined"); + } + } +// TODO: add item classification (kg, m^3, ...) +// if (isset($item->task_id)) { +// $this->orderxdocument->setDocumentPositionQuantity($item->quantity, "HUR"); +// } else { +// $this->orderxdocument->setDocumentPositionQuantity($item->quantity, "H87"); +// } + $linenetamount = $item->line_total; + if ($item->discount > 0) { + if ($this->document->is_amount_discount) { + $linenetamount -= $item->discount; + } else { + $linenetamount -= $linenetamount * ($item->discount / 100); + } + } + $this->orderxdocument->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))) { + $taxtype = $this->getTaxType($item->tax_id); + if (!empty($item->tax_name1)) { + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1); + $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate1); + } elseif (!empty($item->tax_name2)) { + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2); + $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate2); + } elseif (!empty($item->tax_name3)) { + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3); + $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate3); + } else { + nlog("Can't add correct tax position"); + } + } else { + if (!empty($this->document->tax_name1)) { + $taxtype = $this->getTaxType($this->document->tax_name1); + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate1); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate1); + } elseif (!empty($this->document->tax_name2)) { + $taxtype = $this->getTaxType($this->document->tax_name2); + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate2); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate2); + } elseif (!empty($this->document->tax_name3)) { + $taxtype = $this->getTaxType($this->document->tax_name3); + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate3); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate3); + } else { + $taxtype = OrderDutyTaxFeeCategories::ZERO_RATED_GOODS; + $this->orderxdocument->addDocumentPositionTax($taxtype, 'VAT', 0); + $this->addtoTaxMap($taxtype, $linenetamount, 0); + // nlog("Can't add correct tax position"); + } + } + } + + $this->orderxdocument->setDocumentSummation($this->document->amount, $this->document->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->document->amount - $this->document->balance); + + foreach ($this->tax_map as $item) { + $this->orderxdocument->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100); + } + + // The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload + return $this; + + } + + /** + * Returns the XML document + * in string format + * + * @return string + */ + public function getXml(): string + { + return $this->orderxdocument->getContent(); + } + + private function getTaxType($name): string + { + $tax_type = 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: + $tax_type = OrderDutyTaxFeeCategories::STANDARD_RATE; + break; + case Product::PRODUCT_TYPE_EXEMPT: + $tax_type = OrderDutyTaxFeeCategories::EXEMPT_FROM_TAX; + break; + case Product::PRODUCT_TYPE_ZERO_RATED: + $tax_type = OrderDutyTaxFeeCategories::ZERO_RATED_GOODS; + break; + case Product::PRODUCT_TYPE_REVERSE_TAX: + $tax_type = OrderDutyTaxFeeCategories::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($tax_type)) { + if ((in_array($this->document->company->country()->iso_3166_2, $eu_states) && in_array($this->document->client->country->iso_3166_2, $eu_states)) && $this->document->company->country()->iso_3166_2 != $this->document->client->country->iso_3166_2) { + $tax_type = OrderDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; + } elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) { + $tax_type = OrderDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX; + } elseif ($this->document->client->country->iso_3166_2 == "ES-CN") { + $tax_type = OrderDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; + } elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { + $tax_type = OrderDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA; + } else { + nlog("Unkown tax case for xinvoice"); + $tax_type = OrderDutyTaxFeeCategories::STANDARD_RATE; + } + } + return $tax_type; + } + private function addtoTaxMap(string $tax_type, float $net_amount, float $tax_rate): void + { + $hash = hash("md5", $tax_type."-".$tax_rate); + if (array_key_exists($hash, $this->tax_map)) { + $this->tax_map[$hash]["net_amount"] += $net_amount; + } else { + $this->tax_map[$hash] = [ + "tax_type" => $tax_type, + "net_amount" => $net_amount, + "tax_rate" => $tax_rate / 100 + ]; + } + } + +} diff --git a/app/Services/Invoice/EInvoice/RoEInvoice.php b/app/Services/EDocument/Standards/RoEInvoice.php similarity index 99% rename from app/Services/Invoice/EInvoice/RoEInvoice.php rename to app/Services/EDocument/Standards/RoEInvoice.php index d01aae492022..4a8ce29969e9 100644 --- a/app/Services/Invoice/EInvoice/RoEInvoice.php +++ b/app/Services/EDocument/Standards/RoEInvoice.php @@ -9,28 +9,28 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Services\Invoice\EInvoice; +namespace App\Services\EInvoicing\Standards; use App\Models\Invoice; use App\Services\AbstractService; use CleverIt\UBL\Invoice\Address; +use CleverIt\UBL\Invoice\ClassifiedTaxCategory; use CleverIt\UBL\Invoice\Contact; use CleverIt\UBL\Invoice\Country; use CleverIt\UBL\Invoice\Generator; use CleverIt\UBL\Invoice\Invoice as UBLInvoice; use CleverIt\UBL\Invoice\InvoiceLine; use CleverIt\UBL\Invoice\Item; +use CleverIt\UBL\Invoice\LegalEntity; use CleverIt\UBL\Invoice\LegalMonetaryTotal; use CleverIt\UBL\Invoice\Party; +use CleverIt\UBL\Invoice\PayeeFinancialAccount; +use CleverIt\UBL\Invoice\PaymentMeans; +use CleverIt\UBL\Invoice\Price; use CleverIt\UBL\Invoice\TaxCategory; use CleverIt\UBL\Invoice\TaxScheme; use CleverIt\UBL\Invoice\TaxSubTotal; use CleverIt\UBL\Invoice\TaxTotal; -use CleverIt\UBL\Invoice\PaymentMeans; -use CleverIt\UBL\Invoice\PayeeFinancialAccount; -use CleverIt\UBL\Invoice\LegalEntity; -use CleverIt\UBL\Invoice\ClassifiedTaxCategory; -use CleverIt\UBL\Invoice\Price; class RoEInvoice extends AbstractService { diff --git a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php b/app/Services/EDocument/Standards/ZugferdEDokument.php similarity index 56% rename from app/Services/Invoice/EInvoice/ZugferdEInvoice.php rename to app/Services/EDocument/Standards/ZugferdEDokument.php index 59ab32605a58..7c6a754904e1 100644 --- a/app/Services/Invoice/EInvoice/ZugferdEInvoice.php +++ b/app/Services/EDocument/Standards/ZugferdEDokument.php @@ -9,28 +9,31 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Services\Invoice\EInvoice; +namespace App\Services\EInvoicing\Standards; +use App\Models\Credit; use App\Models\Invoice; use App\Models\Product; +use App\Models\PurchaseOrder; +use App\Models\Quote; use App\Services\AbstractService; use horstoeko\zugferd\codelists\ZugferdDutyTaxFeeCategories; use horstoeko\zugferd\ZugferdDocumentBuilder; use horstoeko\zugferd\ZugferdProfiles; -class ZugferdEInvoice extends AbstractService +class ZugferdEDokument extends AbstractService { - public ZugferdDocumentBuilder $xrechnung; + public ZugferdDocumentBuilder $xdocument; - public function __construct(public Invoice $invoice, private readonly bool $returnObject = false, private array $tax_map = []) + public function __construct(public object $document, private readonly bool $returnObject = false, private array $tax_map = []) { } public function run(): self { - $company = $this->invoice->company; - $client = $this->invoice->client; + $company = $this->document->company; + $client = $this->document->client; $profile = $client->getSetting('e_invoice_type'); $profile = match ($profile) { @@ -46,123 +49,143 @@ class ZugferdEInvoice extends AbstractService default => ZugferdProfiles::PROFILE_EN16931, }; - $this->xrechnung = ZugferdDocumentBuilder::CreateNew($profile); + $this->xdocument = ZugferdDocumentBuilder::CreateNew($profile); - $this->xrechnung - ->setDocumentSupplyChainEvent(date_create($this->invoice->date ?? now()->format('Y-m-d'))) + $this->xdocument + ->setDocumentSupplyChainEvent(date_create($this->document->date ?? now()->format('Y-m-d'))) ->setDocumentSeller($company->getSetting('name')) ->setDocumentSellerAddress($company->getSetting("address1"), $company->getSetting("address2"), "", $company->getSetting("postal_code"), $company->getSetting("city"), $company->country()->iso_3166_2, $company->getSetting("state")) - ->setDocumentSellerContact($this->invoice->user->present()->getFullName(), "", $this->invoice->user->present()->phone(), "", $this->invoice->user->email) + ->setDocumentSellerContact($this->document->user->present()->getFullName(), "", $this->document->user->present()->phone(), "", $this->document->user->email) ->setDocumentBuyer($client->present()->name(), $client->number) ->setDocumentBuyerAddress($client->address1, "", "", $client->postal_code, $client->city, $client->country->iso_3166_2, $client->state) ->setDocumentBuyerContact($client->present()->primary_contact_name(), "", $client->present()->phone(), "", $client->present()->email()) - ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->invoice->date ?? now()->format('Y-m-d'))->diff(date_create($this->invoice->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->invoice->due_date])); + ->addDocumentPaymentTerm(ctrans("texts.xinvoice_payable", ['payeddue' => date_create($this->document->date ?? now()->format('Y-m-d'))->diff(date_create($this->document->due_date ?? now()->format('Y-m-d')))->format("%d"), 'paydate' => $this->document->due_date])); - if (!empty($this->invoice->public_notes)) { - $this->xrechnung->addDocumentNote($this->invoice->public_notes ?? ''); + if (!empty($this->document->public_notes)) { + $this->xdocument->addDocumentNote($this->document->public_notes ?? ''); } - if (empty($this->invoice->number)) { - $this->xrechnung->setDocumentInformation("DRAFT", "380", date_create($this->invoice->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); - } else { - $this->xrechnung->setDocumentInformation($this->invoice->number, "380", date_create($this->invoice->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + // Document type + $document_class = get_class($this->document); + switch ($document_class){ + case Quote::class: + // Probably wrong file code https://github.com/horstoeko/zugferd/blob/master/src/codelists/ZugferdInvoiceType.php + if (empty($this->document->number)) { + $this->xdocument->setDocumentInformation("DRAFT", "84", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + } else { + $this->xdocument->setDocumentInformation($this->document->number, "84", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + }; + break; + case Invoice::class: + if (empty($this->document->number)) { + $this->xdocument->setDocumentInformation("DRAFT", "380", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + } else { + $this->xdocument->setDocumentInformation($this->document->number, "380", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + } + break; + case Credit::class: + if (empty($this->document->number)) { + $this->xdocument->setDocumentInformation("DRAFT", "389", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + } else { + $this->xdocument->setDocumentInformation($this->document->number, "389", date_create($this->document->date ?? now()->format('Y-m-d')), $client->getCurrencyCode()); + } } - if (isset($this->invoice->po_number)) { - $this->xrechnung->setDocumentBuyerOrderReferencedDocument($this->invoice->po_number); + if (isset($this->document->po_number)) { + $this->xdocument->setDocumentBuyerOrderReferencedDocument($this->document->po_number); } if (empty($client->routing_id)) { - $this->xrechnung->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); + $this->xdocument->setDocumentBuyerReference(ctrans("texts.xinvoice_no_buyers_reference")); } else { - $this->xrechnung->setDocumentBuyerReference($client->routing_id); + $this->xdocument->setDocumentBuyerReference($client->routing_id); } if (isset($client->shipping_address1) && $client->shipping_country) { - $this->xrechnung->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state); + $this->xdocument->setDocumentShipToAddress($client->shipping_address1, $client->shipping_address2, "", $client->shipping_postal_code, $client->shipping_city, $client->shipping_country->iso_3166_2, $client->shipping_state); } - $this->xrechnung->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); + $this->xdocument->addDocumentPaymentMean(68, ctrans("texts.xinvoice_online_payment")); if (str_contains($company->getSetting('vat_number'), "/")) { - $this->xrechnung->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); + $this->xdocument->addDocumentSellerTaxRegistration("FC", $company->getSetting('vat_number')); } else { - $this->xrechnung->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); + $this->xdocument->addDocumentSellerTaxRegistration("VA", $company->getSetting('vat_number')); } - $invoicing_data = $this->invoice->calc(); + $invoicing_data = $this->document->calc(); //Create line items and calculate taxes - foreach ($this->invoice->line_items as $index => $item) { + foreach ($this->document->line_items as $index => $item) { /** @var \App\DataMapper\InvoiceItem $item **/ - $this->xrechnung->addNewPosition($index) + $this->xdocument->addNewPosition($index) ->setDocumentPositionGrossPrice($item->gross_line_total) ->setDocumentPositionNetPrice($item->line_total); if (!empty($item->product_key)) { if (!empty($item->notes)) { - $this->xrechnung->setDocumentPositionProductDetails($item->product_key, $item->notes); + $this->xdocument->setDocumentPositionProductDetails($item->product_key, $item->notes); } else { - $this->xrechnung->setDocumentPositionProductDetails($item->product_key); + $this->xdocument->setDocumentPositionProductDetails($item->product_key); } } else { if (!empty($item->notes)) { - $this->xrechnung->setDocumentPositionProductDetails($item->notes); + $this->xdocument->setDocumentPositionProductDetails($item->notes); } else { - $this->xrechnung->setDocumentPositionProductDetails("no product name defined"); + $this->xdocument->setDocumentPositionProductDetails("no product name defined"); } } - if (isset($item->task_id)) { - $this->xrechnung->setDocumentPositionQuantity($item->quantity, "HUR"); + if ($item->type_id == 2) { + $this->xdocument->setDocumentPositionQuantity($item->quantity, "HUR"); } else { - $this->xrechnung->setDocumentPositionQuantity($item->quantity, "H87"); + $this->xdocument->setDocumentPositionQuantity($item->quantity, "H87"); } $linenetamount = $item->line_total; if ($item->discount > 0) { - if ($this->invoice->is_amount_discount) { + if ($this->document->is_amount_discount) { $linenetamount -= $item->discount; } else { $linenetamount -= $linenetamount * ($item->discount / 100); } } - $this->xrechnung->setDocumentPositionLineSummation($linenetamount); + $this->xdocument->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))) { $taxtype = $this->getTaxType($item->tax_id); if (!empty($item->tax_name1)) { - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1); $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate1); } elseif (!empty($item->tax_name2)) { - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate2); $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate2); } elseif (!empty($item->tax_name3)) { - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate3); $this->addtoTaxMap($taxtype, $linenetamount, $item->tax_rate3); } else { // nlog("Can't add correct tax position"); } } else { - if (!empty($this->invoice->tax_name1)) { - $taxtype = $this->getTaxType($this->invoice->tax_name1); - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate1); - $this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate1); - } elseif (!empty($this->invoice->tax_name2)) { - $taxtype = $this->getTaxType($this->invoice->tax_name2); - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate2); - $this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate2); - } elseif (!empty($this->invoice->tax_name3)) { - $taxtype = $this->getTaxType($this->invoice->tax_name3); - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', $this->invoice->tax_rate3); - $this->addtoTaxMap($taxtype, $linenetamount, $this->invoice->tax_rate3); + if (!empty($this->document->tax_name1)) { + $taxtype = $this->getTaxType($this->document->tax_name1); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate1); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate1); + } elseif (!empty($this->document->tax_name2)) { + $taxtype = $this->getTaxType($this->document->tax_name2); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate2); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate2); + } elseif (!empty($this->document->tax_name3)) { + $taxtype = $this->getTaxType($this->document->tax_name3); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $this->document->tax_rate3); + $this->addtoTaxMap($taxtype, $linenetamount, $this->document->tax_rate3); } else { $taxtype = ZugferdDutyTaxFeeCategories::ZERO_RATED_GOODS; - $this->xrechnung->addDocumentPositionTax($taxtype, 'VAT', 0); + $this->xdocument->addDocumentPositionTax($taxtype, 'VAT', 0); $this->addtoTaxMap($taxtype, $linenetamount, 0); // nlog("Can't add correct tax position"); } } } - $this->xrechnung->setDocumentSummation($this->invoice->amount, $this->invoice->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->invoice->amount - $this->invoice->balance); + $this->xdocument->setDocumentSummation($this->document->amount, $this->document->balance, $invoicing_data->getSubTotal(), $invoicing_data->getTotalSurcharges(), $invoicing_data->getTotalDiscount(), $invoicing_data->getSubTotal(), $invoicing_data->getItemTotalTaxes(), 0.0, $this->document->amount - $this->document->balance); foreach ($this->tax_map as $item) { - $this->xrechnung->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100); + $this->xdocument->addDocumentTax($item["tax_type"], "VAT", $item["net_amount"], $item["tax_rate"] * $item["net_amount"], $item["tax_rate"] * 100); } // The validity can be checked using https://portal3.gefeg.com/invoice/validation or https://e-rechnung.bayern.de/app/#/upload @@ -178,7 +201,7 @@ class ZugferdEInvoice extends AbstractService */ public function getXml(): string { - return $this->xrechnung->getContent(); + return $this->xdocument->getContent(); } private function getTaxType($name): string @@ -204,13 +227,13 @@ class ZugferdEInvoice extends AbstractService } $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($tax_type)) { - if ((in_array($this->invoice->company->country()->iso_3166_2, $eu_states) && in_array($this->invoice->client->country->iso_3166_2, $eu_states)) && $this->invoice->company->country()->iso_3166_2 != $this->invoice->client->country->iso_3166_2) { + if ((in_array($this->document->company->country()->iso_3166_2, $eu_states) && in_array($this->document->client->country->iso_3166_2, $eu_states)) && $this->document->company->country()->iso_3166_2 != $this->document->client->country->iso_3166_2) { $tax_type = ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES; - } elseif (!in_array($this->invoice->client->country->iso_3166_2, $eu_states)) { + } elseif (!in_array($this->document->client->country->iso_3166_2, $eu_states)) { $tax_type = ZugferdDutyTaxFeeCategories::SERVICE_OUTSIDE_SCOPE_OF_TAX; - } elseif ($this->invoice->client->country->iso_3166_2 == "ES-CN") { + } elseif ($this->document->client->country->iso_3166_2 == "ES-CN") { $tax_type = ZugferdDutyTaxFeeCategories::CANARY_ISLANDS_GENERAL_INDIRECT_TAX; - } elseif (in_array($this->invoice->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { + } elseif (in_array($this->document->client->country->iso_3166_2, ["ES-CE", "ES-ML"])) { $tax_type = ZugferdDutyTaxFeeCategories::TAX_FOR_PRODUCTION_SERVICES_AND_IMPORTATION_IN_CEUTA_AND_MELILLA; } else { nlog("Unkown tax case for xinvoice"); diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index 04b945ff47c8..5346c0636fe6 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -17,6 +17,8 @@ use App\Jobs\Invoice\CreateUbl; use App\Models\Account; use App\Models\Expense; use App\Models\Invoice; +use App\Models\PurchaseOrder; +use App\Models\Quote; use App\Models\Task; use App\Utils\Ninja; use App\Utils\Traits\MakesHash; @@ -318,7 +320,7 @@ class EmailDefaults } } /** E-Invoice xml file */ - if ($this->email->email_object->settings->enable_e_invoice && $this->email->email_object->entity instanceof Invoice) { + if ($this->email->email_object->settings->enable_e_invoice && ! $this->email->email_object->entity instanceof PurchaseOrder) { $xml_string = $this->email->email_object->entity->service()->getEInvoice(); if($xml_string) { diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index dbe57e361b3c..c89c246a49da 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -11,21 +11,21 @@ namespace App\Services\Invoice; -use App\Models\Task; -use App\Utils\Ninja; +use App\Events\Invoice\InvoiceWasArchived; +use App\Jobs\EDocument\CreateEDocument; +use App\Jobs\Entity\CreateRawPdf; +use App\Jobs\Inventory\AdjustProductInventory; +use App\Libraries\Currency\Conversion\CurrencyApi; +use App\Models\CompanyGateway; use App\Models\Expense; use App\Models\Invoice; use App\Models\Payment; use App\Models\Subscription; -use App\Models\CompanyGateway; -use Illuminate\Support\Carbon; +use App\Models\Task; +use App\Utils\Ninja; use App\Utils\Traits\MakesHash; -use App\Jobs\Entity\CreateRawPdf; -use App\Jobs\Invoice\CreateEInvoice; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Storage; -use App\Events\Invoice\InvoiceWasArchived; -use App\Jobs\Inventory\AdjustProductInventory; -use App\Libraries\Currency\Conversion\CurrencyApi; class InvoiceService { @@ -201,7 +201,7 @@ class InvoiceService public function getEInvoice($contact = null) { - return (new CreateEInvoice($this->invoice))->handle(); + return (new CreateEDocument($this->invoice))->handle(); } public function sendEmail($contact = null) @@ -409,12 +409,12 @@ class InvoiceService $this->invoice->invitations->each(function ($invitation) { try { // 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")); + Storage::disk(config('filesystems.default'))->delete($this->invoice->client->e_document_filepath($invitation).$this->invoice->getFileName("xml")); // } // if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { if (Ninja::isHosted()) { - Storage::disk('public')->delete($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml")); + Storage::disk('public')->delete($this->invoice->client->e_document_filepath($invitation).$this->invoice->getFileName("xml")); } } catch (\Exception $e) { nlog($e->getMessage()); diff --git a/app/Services/Pdf/PdfService.php b/app/Services/Pdf/PdfService.php index e0db7d406419..4a579db80c52 100644 --- a/app/Services/Pdf/PdfService.php +++ b/app/Services/Pdf/PdfService.php @@ -11,7 +11,7 @@ namespace App\Services\Pdf; -use App\Jobs\Invoice\CreateEInvoice; +use App\Jobs\EDocument\CreateEDocument; use App\Models\Company; use App\Models\CreditInvitation; use App\Models\Invoice; @@ -216,7 +216,7 @@ class PdfService { try { - $e_rechnung = (new CreateEInvoice($this->config->entity, true))->handle(); + $e_rechnung = (new CreateEDocument($this->config->entity, true))->handle(); $pdfBuilder = new ZugferdDocumentPdfBuilder($e_rechnung, $pdf); $pdfBuilder->generateDocument(); diff --git a/app/Services/PurchaseOrder/PurchaseOrderService.php b/app/Services/PurchaseOrder/PurchaseOrderService.php index b7bc07cb2c77..cdcf0517842c 100644 --- a/app/Services/PurchaseOrder/PurchaseOrderService.php +++ b/app/Services/PurchaseOrder/PurchaseOrderService.php @@ -11,8 +11,11 @@ namespace App\Services\PurchaseOrder; +use App\Jobs\EDocument\CreateEDocument; use App\Models\PurchaseOrder; +use App\Utils\Ninja; use App\Utils\Traits\MakesHash; +use Illuminate\Support\Facades\Storage; class PurchaseOrderService { @@ -75,6 +78,32 @@ class PurchaseOrderService return (new GetPurchaseOrderPdf($this->purchase_order, $contact))->run(); } + public function getEPurchaseOrder($contact = null) + { + return (new CreateEDocument($this->purchase_order))->handle(); + } + public function deleteEPurchaseOrder() + { + $this->purchase_order->load('invitations'); + + $this->purchase_order->invitations->each(function ($invitation) { + try { + // 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->purchase_order->vendor->e_document_filepath($invitation).$this->purchase_order->getFileName("xml")); + // } + + // if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + if (Ninja::isHosted()) { + Storage::disk('public')->delete($this->purchase_order->->vendor->e_document_filepath($invitation).$this->purchase_order->getFileName("xml")); + } + } catch (\Exception $e) { + nlog($e->getMessage()); + } + }); + + return $this; + } + public function setStatus($status) { $this->purchase_order->status_id = $status; diff --git a/app/Services/Quote/QuoteService.php b/app/Services/Quote/QuoteService.php index ddfebbe00b0d..f2ee47666a67 100644 --- a/app/Services/Quote/QuoteService.php +++ b/app/Services/Quote/QuoteService.php @@ -13,6 +13,7 @@ namespace App\Services\Quote; use App\Events\Quote\QuoteWasApproved; use App\Exceptions\QuoteConversion; +use App\Jobs\EDocument\CreateEDocument; use App\Models\Project; use App\Models\Quote; use App\Repositories\QuoteRepository; @@ -72,6 +73,11 @@ class QuoteService return (new GetQuotePdf($this->quote, $contact))->run(); } + public function getEQuote($contact = null) + { + return (new CreateEDocument($this->quote))->handle(); + } + public function sendEmail($contact = null): self { $send_email = new SendEmail($this->quote, null, $contact); @@ -226,6 +232,27 @@ class QuoteService return $this; } + public function deleteEQuote() + { + $this->quote->load('invitations'); + + $this->quote->invitations->each(function ($invitation) { + try { + // 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->quote->client->e_document_filepath($invitation).$this->quote->getFileName("xml")); + // } + + // if (Ninja::isHosted() && Storage::disk('public')->exists($this->invoice->client->e_invoice_filepath($invitation).$this->invoice->getFileName("xml"))) { + if (Ninja::isHosted()) { + Storage::disk('public')->delete($this->quote->client->e_document_filepath($invitation).$this->quote->getFileName("xml")); + } + } catch (\Exception $e) { + nlog($e->getMessage()); + } + }); + + return $this; + } /** * Saves the quote. diff --git a/composer.json b/composer.json index ea5fdc18ed6d..acb8432d627b 100644 --- a/composer.json +++ b/composer.json @@ -55,6 +55,7 @@ "hashids/hashids": "^4.0", "hedii/laravel-gelf-logger": "^8", "horstoeko/zugferd": "^1", + "horstoeko/orderx": "^1", "imdhemy/laravel-purchases": "^1.7", "intervention/image": "^2.5", "invoiceninja/inspector": "^2.0", @@ -112,7 +113,6 @@ "barryvdh/laravel-ide-helper": "^2.13", "beyondcode/laravel-query-detector": "^1.8", "brianium/paratest": "^7", - "fakerphp/faker": "^1.14", "filp/whoops": "^2.7", "friendsofphp/php-cs-fixer": "^3.14", "laracasts/cypress": "^3.0", diff --git a/composer.lock b/composer.lock index 9dec41427fb6..23fc78064892 100644 --- a/composer.lock +++ b/composer.lock @@ -6,6 +6,46 @@ ], "content-hash": "ef338cb66991ec0e28b96643ac5a5c6f", "packages": [ + { + "name": "adrienrn/php-mimetyper", + "version": "0.2.2", + "source": { + "type": "git", + "url": "https://github.com/adrienrn/php-mimetyper.git", + "reference": "702e00a604b4baed34d69730ce055e05c0f43932" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/adrienrn/php-mimetyper/zipball/702e00a604b4baed34d69730ce055e05c0f43932", + "reference": "702e00a604b4baed34d69730ce055e05c0f43932", + "shasum": "" + }, + "require": { + "dflydev/apache-mime-types": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "MimeTyper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hussard", + "email": "adrien.ricartnoblet@gmail.com" + } + ], + "description": "PHP mime type and extension mapping library: compatible with Symfony, powered by jshttp/mime-db", + "support": { + "issues": "https://github.com/adrienrn/php-mimetyper/issues", + "source": "https://github.com/adrienrn/php-mimetyper/tree/0.2.2" + }, + "time": "2018-09-27T09:45:05+00:00" + }, { "name": "afosto/yaac", "version": "v1.5.2", @@ -2019,6 +2059,65 @@ }, "time": "2022-09-20T18:15:38+00:00" }, + { + "name": "dflydev/apache-mime-types", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-apache-mime-types.git", + "reference": "f30a57e59b7476e4c5270b6a0727d79c9c0eb861" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-apache-mime-types/zipball/f30a57e59b7476e4c5270b6a0727d79c9c0eb861", + "reference": "f30a57e59b7476e4c5270b6a0727d79c9c0eb861", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "twig/twig": "1.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-0": { + "Dflydev\\ApacheMimeTypes": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + } + ], + "description": "Apache MIME Types", + "keywords": [ + "apache", + "mime", + "mimetypes" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-apache-mime-types/issues", + "source": "https://github.com/dflydev/dflydev-apache-mime-types/tree/v1.0.1" + }, + "time": "2013-05-14T02:02:01+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.2", @@ -4302,6 +4401,74 @@ }, "time": "2023-09-22T20:17:48+00:00" }, + { + "name": "horstoeko/orderx", + "version": "v1.0.18", + "source": { + "type": "git", + "url": "https://github.com/horstoeko/orderx.git", + "reference": "0a8535c1cda5574d31e8002e7d03f8bbaafd30ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/horstoeko/orderx/zipball/0a8535c1cda5574d31e8002e7d03f8bbaafd30ed", + "reference": "0a8535c1cda5574d31e8002e7d03f8bbaafd30ed", + "shasum": "" + }, + "require": { + "adrienrn/php-mimetyper": "^0.2", + "ext-simplexml": "*", + "goetas-webservices/xsd2php-runtime": "^0.2.13", + "horstoeko/stringmanagement": "^1", + "jms/serializer": "^3", + "php": "^7.3|^7.4|^8", + "setasign/fpdf": "^1", + "setasign/fpdi": "^2", + "smalot/pdfparser": "^0", + "symfony/validator": "^5|^6", + "symfony/yaml": "^5|^6" + }, + "require-dev": { + "goetas-webservices/xsd2php": "^0", + "pdepend/pdepend": "^2", + "phploc/phploc": "^7", + "phpmd/phpmd": "^2", + "phpstan/phpstan": "^1.8", + "phpunit/phpunit": "^9", + "sebastian/phpcpd": "^6", + "squizlabs/php_codesniffer": "^3" + }, + "type": "package", + "autoload": { + "psr-4": { + "horstoeko\\orderx\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Erling", + "email": "daniel@erling.com.de", + "role": "lead" + } + ], + "description": "A library for creating and reading Order-X document", + "homepage": "https://github.com/horstoeko/orderx", + "keywords": [ + "electronic", + "order", + "order-x", + "orderx" + ], + "support": { + "issues": "https://github.com/horstoeko/orderx/issues", + "source": "https://github.com/horstoeko/orderx/tree/v1.0.18" + }, + "time": "2024-01-27T09:26:23+00:00" + }, { "name": "horstoeko/stringmanagement", "version": "v1.0.11", @@ -11444,24 +11611,27 @@ }, { "name": "smalot/pdfparser", - "version": "v2.9.0", + "version": "v0.19.0", "source": { "type": "git", "url": "https://github.com/smalot/pdfparser.git", - "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761" + "reference": "1895c17417aefa4508e35836c46da61988d61f26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/smalot/pdfparser/zipball/6b53144fcb24af77093d4150dd7d0dd571f25761", - "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/1895c17417aefa4508e35836c46da61988d61f26", + "reference": "1895c17417aefa4508e35836c46da61988d61f26", "shasum": "" }, "require": { - "ext-iconv": "*", "ext-zlib": "*", - "php": ">=7.1", + "php": ">=5.6", "symfony/polyfill-mbstring": "^1.18" }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "symfony/phpunit-bridge": "^5.2" + }, "type": "library", "autoload": { "psr-0": { @@ -11489,9 +11659,9 @@ ], "support": { "issues": "https://github.com/smalot/pdfparser/issues", - "source": "https://github.com/smalot/pdfparser/tree/v2.9.0" + "source": "https://github.com/smalot/pdfparser/tree/v0.19.0" }, - "time": "2024-03-01T09:51:10+00:00" + "time": "2021-04-13T08:27:56+00:00" }, { "name": "socialiteproviders/apple", @@ -19534,5 +19704,5 @@ "platform-dev": { "php": "^8.1|^8.2" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/database/migrations/2024_03_19_346785_add_routing_id_to_vendor b/database/migrations/2024_03_19_346785_add_routing_id_to_vendor new file mode 100644 index 000000000000..964571a3c5fb --- /dev/null +++ b/database/migrations/2024_03_19_346785_add_routing_id_to_vendor @@ -0,0 +1,33 @@ +string('routing_id')->default(null)->nullable(); + }); + Schema::table('companies', function (Blueprint $table) { + $table->string('e_quote_type')->default("OrderX_Comfort"); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + + } +}; diff --git a/lang/en/texts.php b/lang/en/texts.php index b3d101fc4542..85ade44d151d 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5102,6 +5102,8 @@ $lang = array( 'drop_files_here' => 'Drop files here', 'upload_files' => 'Upload Files', 'download_e_invoice' => 'Download E-Invoice', + 'download_e_credit' => 'Download E-Credit', + 'download_e_quote' => 'Download E-Quote', 'triangular_tax_info' => 'Intra-community triangular transaction', 'intracommunity_tax_info' => 'Tax-free intra-community delivery', 'reverse_tax_info' => 'Please note that this supply is subject to reverse charge', diff --git a/package-lock.json b/package-lock.json index f1466debb888..1e4593041b24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "@invoiceninja/invoiceninja", + "name": "invoiceninja", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php b/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php index 5159e3314c1e..314f69cca8ee 100644 --- a/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/pdf-slot.blade.php @@ -20,6 +20,40 @@ @endif + @if($entity_type == 'credit' && $settings->enable_e_invoice) + + @endif + @if($entity_type == 'quote' && $settings->enable_e_invoice) + + @endif +{{-- Not implemented yet--}} +{{-- @if($entity_type == 'purchase_order' && $settings->enable_e_invoice) + + @endif--}} @if($html_entity_option)