From 0d4a1b9043cdc34411c0074ef49d7b0c4f2bc240 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Thu, 1 Aug 2024 16:32:35 +1000 Subject: [PATCH] Peppol calculations --- app/Jobs/EDocument/CreateEDocument.php | 7 +- .../EDocument/Gateway/Storecove/Storecove.php | 46 +++++- app/Services/EDocument/Standards/Peppol.php | 96 +++++++---- .../Einvoice/Storecove/StorecoveTest.php | 153 +++++++++++++++++- 4 files changed, 258 insertions(+), 44 deletions(-) diff --git a/app/Jobs/EDocument/CreateEDocument.php b/app/Jobs/EDocument/CreateEDocument.php index 103ab05d9d..7f96af177a 100644 --- a/app/Jobs/EDocument/CreateEDocument.php +++ b/app/Jobs/EDocument/CreateEDocument.php @@ -11,7 +11,6 @@ namespace App\Jobs\EDocument; -use App\Services\EDocument\Standards\RoEInvoice; use App\Utils\Ninja; use App\Models\Quote; use App\Models\Credit; @@ -23,10 +22,12 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use App\Services\EDocument\Standards\Peppol; use horstoeko\zugferd\ZugferdDocumentBuilder; +use App\Services\EDocument\Standards\FatturaPA; +use App\Services\EDocument\Standards\RoEInvoice; use App\Services\EDocument\Standards\OrderXDocument; use App\Services\EDocument\Standards\FacturaEInvoice; -use App\Services\EDocument\Standards\FatturaPA; use App\Services\EDocument\Standards\ZugferdEDokument; class CreateEDocument implements ShouldQueue @@ -68,6 +69,8 @@ class CreateEDocument implements ShouldQueue if ($this->document instanceof Invoice) { switch ($e_document_type) { + case "PEPPOL": + return (new Peppol($this->document))->toXml(); case "FACT1": return (new RoEInvoice($this->document))->generateXml(); case "FatturaPA": diff --git a/app/Services/EDocument/Gateway/Storecove/Storecove.php b/app/Services/EDocument/Gateway/Storecove/Storecove.php index a56ea37000..0ce0a85613 100644 --- a/app/Services/EDocument/Gateway/Storecove/Storecove.php +++ b/app/Services/EDocument/Gateway/Storecove/Storecove.php @@ -95,6 +95,48 @@ class Storecove { // parseStrategy: ubl // } */ + public function sendJsonDocument($document) + { + + + $payload = [ + "legalEntityId" => 290868, + "idempotencyGuid" => \Illuminate\Support\Str::uuid(), + "routing" => [ + "eIdentifiers" => [], + "emails" => ["david@invoiceninja.com"] + ], + // "document" => [ + // 'documentType' => 'invoice', + // "rawDocumentData" => [ + // "document" => base64_encode($document), + // "parse" => true, + // "parseStrategy" => "ubl", + // ], + // ], + "document"=> [ + "documentType" => "invoice", + "invoice" => $document, + ], + ]; + + $uri = "document_submissions"; + + nlog($payload); + + $r = $this->httpClient($uri, (HttpVerb::POST)->value, $payload, $this->getHeaders()); + + nlog($r->body()); + nlog($r->json()); + + if($r->successful()) { + return $r->json()['guid']; + } + + return false; + + } + public function sendDocument($document) { @@ -256,8 +298,6 @@ class Storecove { } - - public function addIdentifier(int $legal_entity_id, string $identifier, string $scheme) { $uri = "legal_entities/{$legal_entity_id}/peppol_identifiers"; @@ -278,7 +318,6 @@ class Storecove { } - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// private function getHeaders(array $headers = []) { @@ -300,5 +339,4 @@ class Storecove { return $r; } - } \ No newline at end of file diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index df803f4214..a0d7b4892f 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -151,14 +151,36 @@ class Peppol extends AbstractService private InvoiceSum | InvoiceSumInclusive $calc; + private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; /** * @param Invoice $invoice */ - public function __construct(public Invoice $invoice, public ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice = null) + public function __construct(public Invoice $invoice) { - $this->p_invoice = $p_invoice ?? new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); $this->company = $invoice->company; $this->calc = $this->invoice->calc(); + $this->setInvoice(); + } + + private function setInvoice(): self + { + + + if($this->invoice->e_invoice){ + + + $e = new EInvoice(); + $this->p_invoice = $e->decode('Peppol', json_encode($this->invoice->e_invoice->Invoice), 'json'); + + return $this; + + } + + $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + + $this->setInvoiceDefaults(); + + return $this; } public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice @@ -171,7 +193,31 @@ class Peppol extends AbstractService public function toXml(): string { $e = new EInvoice(); - return $e->encode($this->p_invoice, 'xml'); + $xml = $e->encode($this->p_invoice, 'xml'); + + $prefix = ' + '; + + return str_ireplace(['\n',''], ['', $prefix], $xml); + + } + + public function toJson(): string + { + $e = new EInvoice(); + $json = $e->encode($this->p_invoice, 'json'); + + return $json; + // $prefixes = str_ireplace(["cac:","cbc:"], "", $json); + // return str_ireplace(["InvoiceLine", "PostalAddress", "PartyName"], ["invoiceLines","address", "companyName"], $prefixes); + } + + public function toArray(): array + { + return json_decode($this->toJson(), true); } public function run() @@ -192,17 +238,6 @@ class Peppol extends AbstractService } - // private function getPaymentMeans(): PaymentMeans - // { - // $payeeFinancialAccount = new PayeeFinancialAccount() - // $payeeFinancialAccount-> - - // $ppm = new PaymentMeans(); - // $ppm->PayeeFinancialAccount = $payeeFinancialAccount; - - // return $ppm; - // } - private function getLegalMonetaryTotal(): LegalMonetaryTotal { $taxable = $this->getTaxable(); @@ -509,7 +544,7 @@ class Peppol extends AbstractService $tax_amount = new TaxAmount(); $tax_amount->currencyID = $this->invoice->client->currency()->code; -$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate3, $item->line_total) : $this->calcAmountLineTax($item->tax_rate3, $item->line_total); + $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate3, $item->line_total) : $this->calcAmountLineTax($item->tax_rate3, $item->line_total); $tax_subtotal = new TaxSubtotal(); $tax_subtotal->TaxAmount = $tax_amount; @@ -724,22 +759,18 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv return $this; } - private function setPaymentMeans(): self + private function setPaymentMeans(bool $required = false): self { - $paymentMeans = new PaymentMeans(); + + if($this->p_invoice->PaymentMeans) + return $this; + elseif(!isset($this->p_invoice->PaymentMeans) && $paymentMeans = $this->getSetting('Invoice.PaymentMeans')){ + $this->p_invoice->PaymentMeans = is_array($paymentMeans) ? $paymentMeans : [$paymentMeans]; + return $this; + } -// = $this->getPaymentMeans(); - -// $payeeFinancialAccount = (new PayeeFinancialAccount()) -// ->setBankId($company->settings->custom_value1) -// ->setBankName($company->settings->custom_value2); - -// $paymentMeans = (new PaymentMeans()) -// ->setPaymentMeansCode($invoice->custom_value1) -// ->setPayeeFinancialAccount($payeeFinancialAccount); -// $ubl_invoice->setPaymentMeans($paymentMeans); - - $this->p_invoice->PaymentMeans = $paymentMeans; + if($required) + throw new \Exception('e-invoice generation halted:: Payment Means required'); return $this; } @@ -748,10 +779,9 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv { // accountingsupplierparty.party.contact MUST be set - Name / Telephone / Electronic Mail // this is forced by default. + + $this->setPaymentMeans(true); - // ONE payment means MUST be set - - // return $this; } @@ -778,7 +808,7 @@ $tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiv private function ES(): self { -// For B2B, provide an ES:DIRE routing identifier and an ES:VAT tax identifier. + // For B2B, provide an ES:DIRE routing identifier and an ES:VAT tax identifier. // both sender and receiver must be an ES company; // you must have a "credit_transfer" PaymentMean; // the "dueDate" property is mandatory. diff --git a/tests/Integration/Einvoice/Storecove/StorecoveTest.php b/tests/Integration/Einvoice/Storecove/StorecoveTest.php index f37af88946..57882e1feb 100644 --- a/tests/Integration/Einvoice/Storecove/StorecoveTest.php +++ b/tests/Integration/Einvoice/Storecove/StorecoveTest.php @@ -11,18 +11,25 @@ namespace Tests\Integration\Einvoice\Storecove; -use App\DataMapper\ClientSettings; -use App\Models\Client; use Tests\TestCase; +use App\Models\Client; +use App\Models\Company; +use App\Models\Invoice; use Tests\MockAccountData; +use App\DataMapper\InvoiceItem; +use App\DataMapper\ClientSettings; +use App\DataMapper\CompanySettings; +use App\Services\EDocument\Standards\Peppol; use Illuminate\Foundation\Testing\DatabaseTransactions; - +use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans; class StorecoveTest extends TestCase { use MockAccountData; use DatabaseTransactions; + private string $routing_id = ''; + protected function setUp(): void { parent::setUp(); @@ -88,7 +95,7 @@ class StorecoveTest extends TestCase // nlog($r); // } - +/* public function testGetLegalEntity() { @@ -348,7 +355,7 @@ $x = ' $sc->sendDocument($x); } - +*/ public function testCreateCHClient() { @@ -390,4 +397,140 @@ $x = ' $this->assertInstanceOf(\App\Models\Client::class, $c); } + + private function createDEData() + { + + $this->routing_id = '290868'; + + $settings = CompanySettings::defaults(); + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.de'; + $settings->address1 = 'Musterstraße 1'; + $settings->address2 = 'Etage 2, Büro 3'; + $settings->city = 'Berlin'; + $settings->state = 'Berlin'; + $settings->postal_code = '10115'; + $settings->phone = '030 1234567'; + $settings->email = $this->faker->unique()->safeEmail(); + $settings->country_id = '276'; // Germany's ISO country code + $settings->vat_number = 'DE123456789'; + $settings->id_number = 'HRB 12345'; + $settings->use_credits_payment = 'always'; + $settings->timezone_id = '1'; // CET (Central European Time) + $settings->entity_send_time = 0; + $settings->e_invoice_type = 'PEPPOL'; + $settings->currency_id = '3'; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $this->user->companies()->attach($company->id, [ + 'account_id' => $this->account->id, + 'is_owner' => true, + 'is_admin' => 1, + 'is_locked' => 0, + 'permissions' => '', + 'notifications' => CompanySettings::notificationAdminDefaults(), + 'settings' => null, + ]); + + Client::unguard(); + + $c = + Client::create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Beispiel Firma GmbH', + 'website' => 'https://www.beispiel-firma.de', + 'private_notes' => 'Dies sind private Notizen zum Testkunden.', + 'balance' => 0, + 'paid_to_date' => 0, + 'vat_number' => 'DE654321987', + 'id_number' => 'HRB 12345', // Typical format for German company registration numbers + 'custom_value1' => '2024-07-22 10:00:00', + 'custom_value2' => 'blau', + 'custom_value3' => 'musterwort', + 'custom_value4' => 'test@example.com', + 'address1' => 'Musterstraße 123', + 'address2' => '2. Etage, Büro 45', + 'city' => 'München', + 'state' => 'Bayern', + 'postal_code' => '80331', + 'country_id' => '276', // Germany + 'shipping_address1' => 'Musterstraße 123', + 'shipping_address2' => '2. Etage, Büro 45', + 'shipping_city' => 'München', + 'shipping_state' => 'Bayern', + 'shipping_postal_code' => '80331', + 'shipping_country_id' => '276', // Germany + 'settings' => ClientSettings::Defaults(), + 'client_hash' => \Illuminate\Support\Str::random(32), + 'routing_id' => '', + ]); + + + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 19; + $item->tax_name1 = 'mwst'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $c->id, + 'discount' => 0, + 'uses_inclusive_taxes' => false, + 'status_id' => 1, + 'tax_rate1' => 0, + 'tax_name1' => '', + 'tax_rate2' => 0, + 'tax_rate3' => 0, + 'tax_name2' => '', + 'tax_name3' => '', + 'line_items' => [$item], + 'number' => 'DE-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d') + ]); + + $invoice = $invoice->calc()->getInvoice(); + $invoice->service()->markSent()->save(); + + + return $invoice; + + } + + public function testDeRules() + { + $invoice = $this->createDEData(); + + $e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice(); + + $stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}'); + foreach($stub as $key => $value) + $e_invoice->{$key} = $value; + + $invoice->e_invoice = $e_invoice; + $invoice->save(); + + $this->assertInstanceOf(Invoice::class, $invoice); + $this->assertInstanceof(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $e_invoice); + + $p = new Peppol($invoice); + + $p->run(); + $xml = $p->toXml(); + nlog($xml); + $sc = new \App\Services\EDocument\Gateway\Storecove\Storecove(); + $sc->sendDocument($xml); + + + } + }