From de874a21cdc9f5bd61c81b34b4289a5897596841 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 7 Jun 2024 14:09:02 +1000 Subject: [PATCH] Peppol tests --- app/Services/EDocument/Standards/Peppol.php | 381 ++++++++++++++++++++ tests/Feature/EInvoice/PeppolTest.php | 154 ++++++++ 2 files changed, 535 insertions(+) create mode 100644 tests/Feature/EInvoice/PeppolTest.php diff --git a/app/Services/EDocument/Standards/Peppol.php b/app/Services/EDocument/Standards/Peppol.php index a39ade52763b..c5f62f843d27 100644 --- a/app/Services/EDocument/Standards/Peppol.php +++ b/app/Services/EDocument/Standards/Peppol.php @@ -11,19 +11,400 @@ namespace App\Services\EDocument\Standards; +use App\Helpers\Invoice\InvoiceSum; +use App\Helpers\Invoice\InvoiceSumInclusive; use App\Models\Invoice; use App\Services\AbstractService; +use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party; +use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address; +use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount; +use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact; +use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country; +use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty; +use InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine; +use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item; +use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal; +use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName; +use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty; +use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory; +use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory; +use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme; +use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme; +use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal; +use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal; class Peppol extends AbstractService { + + private array $InvoiceTypeCodes = [ + "380" => "Commercial invoice", + "381" => "Credit note", + "383" => "Corrected invoice", + "384" => "Prepayment invoice", + "386" => "Proforma invoice", + "875" => "Self-billed invoice", + "976" => "Factored invoice", + "84" => "Invoice for cross border services", + "82" => "Simplified invoice", + "80" => "Debit note", + "875" => "Self-billed credit note", + "896" => "Debit note related to self-billed invoice" + ]; + + private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice; + + private InvoiceSum | InvoiceSumInclusive $calc; + /** * @param Invoice $invoice */ public function __construct(public Invoice $invoice) { + $this->p_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice; + $this->calc = $this->invoice->calc(); + } + + public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice + { + return $this->p_invoice; + } public function run() { + $this->p_invoice->ID = $this->invoice->number; + $this->p_invoice->IssueDate = new \DateTime($this->invoice->date); + $this->p_invoice->InvoiceTypeCode = 380; // + $this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty(); + $this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty(); + $this->p_invoice->InvoiceLine = $this->getInvoiceLines(); + $this->p_invoice->TaxTotal = $this->getTotalTaxes(); + $this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal(); + +// $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); + } + + private function getLegalMonetaryTotal(): LegalMonetaryTotal + { + $taxable = $this->getTaxable(); + + $lmt = new LegalMonetaryTotal; + $lmt->LineExtensionAmount = $taxable; + $lmt->TaxExclusiveAmount = $taxable; + $lmt->TaxInclusiveAmount = $this->invoice->amount; + $lmt->PayableAmount = $this->invoice->amount; + + return $lmt; + } + + private function getTotalTaxes(): array + { + $taxes = []; + + $type_id = $this->invoice->line_items[0]->type_id; + + if( strlen($this->invoice->tax_name1 ?? '') > 1) + { + + $tax_amount = new TaxAmount(); + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate1),2); + $tax_subtotal = new TaxSubtotal(); + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory(); + $tc->ID = $type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $this->invoice->tax_rate1; + $ts = new PeppolTaxScheme(); + $ts->ID = $this->invoice->tax_name1; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + $tax_total = new TaxTotal; + $tax_total->TaxAmount = $tax_amount; + $tax_total->TaxSubtotal = $tax_subtotal; + + $taxes[] = $tax_total; + } + + + if(strlen($this->invoice->tax_name2 ?? '') > 1) { + + $tax_amount = new TaxAmount(); + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate2), 2); + $tax_subtotal = new TaxSubtotal(); + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory(); + $tc->ID = $type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $this->invoice->tax_rate2; + $ts = new PeppolTaxScheme(); + $ts->ID = $this->invoice->tax_name2; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + + $tax_total = new TaxTotal(); + $tax_total->TaxAmount = $tax_amount; + $tax_total->TaxSubtotal = $tax_subtotal; + + $taxes[] = $tax_total; + + } + + if(strlen($this->invoice->tax_name3 ?? '') > 1) { + + $tax_amount = new TaxAmount(); + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round($this->invoice->amount * (1 / $this->invoice->tax_rate1), 2); + $tax_subtotal = new TaxSubtotal(); + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory(); + $tc->ID = $type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $this->invoice->tax_rate3; + $ts = new PeppolTaxScheme(); + $ts->ID = $this->invoice->tax_name3; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + + $tax_total = new TaxTotal(); + $tax_total->TaxAmount = $tax_amount; + $tax_total->TaxSubtotal = $tax_subtotal; + + $taxes[] = $tax_total; + + } + + + return $taxes; + } + + private function getInvoiceLines(): array + { + $lines = []; + + foreach($this->invoice->line_items as $key => $item) + { + + $_item = new Item; + $_item->Name = $item->product_key; + $_item->Description = $item->notes; + + $line = new InvoiceLine; + $line->ID = $key+1; + $line->InvoicedQuantity = $item->quantity; + $line->LineExtensionAmount = $item->line_total; + $line->Item = $_item; + $line->TaxTotal = $this->getItemTaxes($item); + $line->Price = $this->costWithDiscount($item); + + $lines[] = $line; + } + + return $lines; + } + + private function costWithDiscount($item) + { + $cost = $item->cost; + + if ($item->discount != 0) { + if ($this->invoice->is_amount_discount) { + $cost -= $item->discount / $item->quantity; + } else { + $cost -= $cost * $item->discount / 100; + } + } + + return $cost; + } + + private function getItemTaxes(object $item): array + { + $item_taxes = []; + + if(strlen($item->tax_name1 ?? '') > 1) + { + + $tax_amount = new TaxAmount; + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round(($item->line_total * (1/$item->tax_rate1)),2); + $tax_subtotal = new TaxSubtotal; + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory; + $tc->ID = $item->type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $item->tax_rate1; + $ts = new PeppolTaxScheme; + $ts->ID = $item->tax_name1; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + $item_taxes[] = $tax_subtotal; + + } + + + if(strlen($item->tax_name2 ?? '') > 1) { + + $tax_amount = new TaxAmount(); + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round(($item->line_total * (1 / $item->tax_rate2)),2); + $tax_subtotal = new TaxSubtotal(); + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory(); + $tc->ID = $item->type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $item->tax_rate2; + $ts = new PeppolTaxScheme(); + $ts->ID = $item->tax_name2; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + $item_taxes[] = $tax_subtotal; + + } + + + if(strlen($item->tax_name3 ?? '') > 1) { + + $tax_amount = new TaxAmount(); + $tax_amount->currencyID = $this->invoice->client->currency()->code; + $tax_amount->amount = round(($item->line_total * (1 / $item->tax_rate3)),2); + $tax_subtotal = new TaxSubtotal(); + $tax_subtotal->TaxableAmount = $tax_amount; + $tc = new TaxCategory(); + $tc->ID = $item->type_id == '2' ? 'HUR' : 'C62'; + $tc->Percent = $item->tax_rate3; + $ts = new PeppolTaxScheme(); + $ts->ID = $item->tax_name3; + $tc->TaxScheme = $ts; + $tax_subtotal->TaxCategory = $tc; + + $item_taxes[] = $tax_subtotal; + + } + + return $item_taxes; + } + + private function getAccountingSupplierParty(): AccountingSupplierParty + { + + $asp = new AccountingSupplierParty(); + + $party = new Party(); + $party_name = new PartyName; + $party_name->Name = $this->invoice->company->present()->name(); + $party->PartyName[] = $party_name; + + $address = new Address(); + $address->CityName = $this->invoice->company->settings->city; + $address->StreetName = $this->invoice->company->settings->address1; + $address->BuildingName = $this->invoice->company->settings->address2; + $address->PostalZone = $this->invoice->company->settings->postal_code; + + $country = new Country(); + $country->IdentificationCode = $this->invoice->company->country()->iso_3166_2; + $address->Country = $country; + + $party->PostalAddress = $address; + $party->PhysicalLocation = $address; + + $contact = new Contact(); + $contact->ElectronicMail = $this->invoice->company->owner()->email ?? 'owner@gmail.com'; + + $party->Contact = $contact; + + $asp->Party = $party; + + return $asp; + } + + private function getAccountingCustomerParty(): AccountingCustomerParty + { + + $acp = new AccountingCustomerParty(); + + $party = new Party(); + + $party_name = new PartyName(); + $party_name->Name = $this->invoice->client->present()->name(); + $party->PartyName[] = $party_name; + + $address = new Address(); + $address->CityName = $this->invoice->client->city; + $address->StreetName = $this->invoice->client->address1; + $address->BuildingName = $this->invoice->client->address2; + $address->PostalZone = $this->invoice->client->postal_code; + + $country = new Country(); + $country->IdentificationCode = $this->invoice->client->country->iso_3166_2; + $address->Country = $country; + + $party->PostalAddress = $address; + $party->PhysicalLocation = $address; + + $contact = new Contact(); + $contact->ElectronicMail = $this->invoice->client->present()->email(); + + $party->Contact = $contact; + + $acp->Party = $party; + + return $acp; + } + + private function getTaxable(): float + { + $total = 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; + } + } \ No newline at end of file diff --git a/tests/Feature/EInvoice/PeppolTest.php b/tests/Feature/EInvoice/PeppolTest.php new file mode 100644 index 000000000000..1e613c13457b --- /dev/null +++ b/tests/Feature/EInvoice/PeppolTest.php @@ -0,0 +1,154 @@ +makeTestData(); + + + // $this->markTestSkipped('prevent running in CI'); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testInvoiceBoot() + { + + $settings = CompanySettings::defaults(); + $settings->address1 = 'Via Silvio Spaventa 108'; + $settings->city = 'Calcinelli'; + + $settings->state = 'PA'; + + // $settings->state = 'Perugia'; + $settings->postal_code = '61030'; + $settings->country_id = '380'; + $settings->currency_id = '3'; + $settings->vat_number = '01234567890'; + $settings->id_number = ''; + + $company = Company::factory()->create([ + 'account_id' => $this->account->id, + 'settings' => $settings, + ]); + + $client_settings = ClientSettings::defaults(); + $client_settings->currency_id = '3'; + + $client = Client::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'name' => 'Italian Client Name', + 'address1' => 'Via Antonio da Legnago 68', + 'city' => 'Monasterace', + 'state' => 'CR', + // 'state' => 'Reggio Calabria', + 'postal_code' => '89040', + 'country_id' => 380, + 'routing_id' => 'ABC1234', + 'settings' => $client_settings, + ]); + + $item = new InvoiceItem(); + $item->product_key = "Product Key"; + $item->notes = "Product Description"; + $item->cost = 10; + $item->quantity = 10; + $item->tax_rate1 = 22; + $item->tax_name1 = 'IVA'; + + $invoice = Invoice::factory()->create([ + 'company_id' => $company->id, + 'user_id' => $this->user->id, + 'client_id' => $client->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' => 'ITA-'.rand(1000, 100000), + 'date' => now()->format('Y-m-d') + ]); + + $invoice->service()->markSent()->save(); + + $fat = new Peppol($invoice); + $fat->run(); + + $fe = $fat->getInvoice(); + + $this->assertNotNull($fe); + + $this->assertInstanceOf(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $fe); + + $e = new EInvoice(); + $xml = $e->encode($fe, 'xml'); + $this->assertNotNull($xml); + + nlog($xml); + + $json = $e->encode($fe, 'json'); + $this->assertNotNull($json); + + nlog($json); + + $decode = $e->decode('Peppol', $json, 'json'); + + $this->assertInstanceOf(\InvoiceNinja\EInvoice\Models\Peppol\Invoice::class, $decode); + + $errors = $e->validate($fe); + + if(count($errors) > 0) { + nlog($errors); + } + + $this->assertCount(0, $errors); + + + } +}