mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-11 13:42:49 +01:00
538 lines
19 KiB
PHP
538 lines
19 KiB
PHP
<?php
|
|
/**
|
|
* Invoice Ninja (https://invoiceninja.com).
|
|
*
|
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
|
*
|
|
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
|
|
*
|
|
* @license https://www.elastic.co/licensing/elastic-license
|
|
*/
|
|
|
|
namespace App\Services\EDocument\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 App\Models\Product;
|
|
|
|
/**
|
|
* Requirements:
|
|
* FACT1:
|
|
* Bank ID => company->settings->custom_value1
|
|
* Bank Name => company->settings->custom_value2
|
|
* Sector Code => company->settings->state
|
|
* Sub Entity Code => company->settings->city
|
|
* Payment Means => invoice.custom_value1
|
|
*/
|
|
class RoEInvoice extends AbstractService
|
|
{
|
|
|
|
private array $countrySubEntity = [
|
|
'RO-AB' => 'Alba',
|
|
'RO-AG' => 'Argeș',
|
|
'RO-AR' => 'Arad',
|
|
'RO-B' => 'Bucharest',
|
|
'RO-BC' => 'Bacău',
|
|
'RO-BH' => 'Bihor',
|
|
'RO-BN' => 'Bistrița-Năsăud',
|
|
'RO-BR' => 'Brăila',
|
|
'RO-BT' => 'Botoșani',
|
|
'RO-BV' => 'Brașov',
|
|
'RO-BZ' => 'Buzău',
|
|
'RO-CJ' => 'Cluj',
|
|
'RO-CL' => 'Călărași',
|
|
'RO-CS' => 'Caraș-Severin',
|
|
'RO-CT' => 'Constanța',
|
|
'RO-CV' => 'Covasna',
|
|
'RO-DB' => 'Dâmbovița',
|
|
'RO-DJ' => 'Dolj',
|
|
'RO-GJ' => 'Gorj',
|
|
'RO-GL' => 'Galați',
|
|
'RO-GR' => 'Giurgiu',
|
|
'RO-HD' => 'Hunedoara',
|
|
'RO-HR' => 'Harghita',
|
|
'RO-IF' => 'Ilfov',
|
|
'RO-IL' => 'Ialomița',
|
|
'RO-IS' => 'Iași',
|
|
'RO-MH' => 'Mehedinți',
|
|
'RO-MM' => 'Maramureș',
|
|
'RO-MS' => 'Mureș',
|
|
'RO-NT' => 'Neamț',
|
|
'RO-OT' => 'Olt',
|
|
'RO-PH' => 'Prahova',
|
|
'RO-SB' => 'Sibiu',
|
|
'RO-SJ' => 'Sălaj',
|
|
'RO-SM' => 'Satu Mare',
|
|
'RO-SV' => 'Suceava',
|
|
'RO-TL' => 'Tulcea',
|
|
'RO-TM' => 'Timiș',
|
|
'RO-TR' => 'Teleorman',
|
|
'RO-VL' => 'Vâlcea',
|
|
'RO-VN' => 'Vaslui',
|
|
'RO-VS' => 'Vrancea',
|
|
];
|
|
|
|
private array $sectorList = [
|
|
'SECTOR1' => 'Agriculture',
|
|
'SECTOR2' => 'Manufacturing',
|
|
'SECTOR3' => 'Tourism',
|
|
'SECTOR4' => 'Information Technology (IT):',
|
|
'SECTOR5' => 'Energy',
|
|
'SECTOR6' => 'Healthcare',
|
|
'SECTOR7' => 'Education',
|
|
];
|
|
|
|
private array $sectorCodes = [
|
|
'RO-AB' => 'Manufacturing, Agriculture',
|
|
'RO-AG' => 'Manufacturing, Agriculture',
|
|
'RO-AR' => 'Manufacturing, Agriculture',
|
|
'RO-B' => 'Information Technology (IT), Education, Tourism',
|
|
'RO-BC' => 'Manufacturing, Agriculture',
|
|
'RO-BH' => 'Agriculture, Manufacturing',
|
|
'RO-BN' => 'Agriculture',
|
|
'RO-BR' => 'Agriculture',
|
|
'RO-BT' => 'Agriculture',
|
|
'RO-BV' => 'Tourism, Agriculture',
|
|
'RO-BZ' => 'Agriculture',
|
|
'RO-CJ' => 'Information Technology (IT), Education, Tourism',
|
|
'RO-CL' => 'Agriculture',
|
|
'RO-CS' => 'Manufacturing, Agriculture',
|
|
'RO-CT' => 'Tourism, Agriculture',
|
|
'RO-CV' => 'Agriculture',
|
|
'RO-DB' => 'Agriculture',
|
|
'RO-DJ' => 'Agriculture',
|
|
'RO-GJ' => 'Manufacturing, Agriculture',
|
|
'RO-GL' => 'Energy, Manufacturing',
|
|
'RO-GR' => 'Agriculture',
|
|
'RO-HD' => 'Energy, Manufacturing',
|
|
'RO-HR' => 'Agriculture',
|
|
'RO-IF' => 'Information Technology (IT), Education',
|
|
'RO-IL' => 'Agriculture',
|
|
'RO-IS' => 'Information Technology (IT), Education, Agriculture',
|
|
'RO-MH' => 'Manufacturing, Agriculture',
|
|
'RO-MM' => 'Agriculture',
|
|
'RO-MS' => 'Energy, Manufacturing, Agriculture',
|
|
'RO-NT' => 'Agriculture',
|
|
'RO-OT' => 'Agriculture',
|
|
'RO-PH' => 'Energy, Manufacturing',
|
|
'RO-SB' => 'Manufacturing, Agriculture',
|
|
'RO-SJ' => 'Agriculture',
|
|
'RO-SM' => 'Agriculture',
|
|
'RO-SV' => 'Agriculture',
|
|
'RO-TL' => 'Agriculture',
|
|
'RO-TM' => 'Agriculture, Manufacturing',
|
|
'RO-TR' => 'Agriculture',
|
|
'RO-VL' => 'Agriculture',
|
|
'RO-VN' => 'Agriculture',
|
|
'RO-VS' => 'Agriculture',
|
|
];
|
|
|
|
public function __construct(public Invoice $invoice)
|
|
{
|
|
}
|
|
|
|
private function resolveSubEntityCode(string $city)
|
|
{
|
|
$city_references = &$this->countrySubEntity[$city];
|
|
|
|
return $city_references ?? 'RO-B';
|
|
}
|
|
|
|
private function resolveSectorCode(string $state)
|
|
{
|
|
return in_array($state, $this->sectorList) ? $state : 'SECTOR1';
|
|
}
|
|
|
|
/**
|
|
* Execute the job
|
|
* @return UBLInvoice
|
|
*/
|
|
public function run(): UBLInvoice
|
|
{
|
|
$invoice = $this->invoice;
|
|
$company = $invoice->company;
|
|
$client = $invoice->client;
|
|
$companyVatNr = $company->settings->vat_number;
|
|
$clientVatNr = $client->vat_number;
|
|
$companyIdn = $company->settings->id_number;
|
|
$clientIdn = $client->id_number;
|
|
$coUserFirstName = $company->owner()->present()->firstName();
|
|
$coUserLastName = $company->owner()->present()->lastName();
|
|
$coFullName = $coUserFirstName . ' ' . $coUserLastName;
|
|
$clUserFirstName = $client->present()->first_name();
|
|
$clUserLastName = $client->present()->last_name();
|
|
$clFullName = $clUserFirstName . ' ' . $clUserLastName;
|
|
$coEmail = $company->settings->email;
|
|
$coPhone = $company->settings->phone;
|
|
$clPhone = $client->present()->phone();
|
|
$clEmail = $client->present()->email();
|
|
|
|
$ubl_invoice = new UBLInvoice();
|
|
|
|
$ubl_invoice->setCustomizationID("urn:cen.eu:en16931:2017#compliant#urn:efactura.mfinante.ro:CIUS-RO:1.0.1");
|
|
// invoice
|
|
$ubl_invoice->setId($invoice->number);
|
|
$ubl_invoice->setIssueDate(date_create($invoice->date));
|
|
$ubl_invoice->setDueDate(date_create($invoice->due_date));
|
|
$ubl_invoice->setInvoiceTypeCode("380");
|
|
$ubl_invoice->setDocumentCurrencyCode($invoice->client->getCurrencyCode());
|
|
$ubl_invoice->setTaxCurrencyCode($invoice->client->getCurrencyCode());
|
|
|
|
foreach ($invoice->line_items as $index => $item) {
|
|
|
|
if (!empty($item->tax_name1)) {
|
|
$taxName = $item->tax_name1;
|
|
} elseif (!empty($item->tax_name2)) {
|
|
$taxName = $item->tax_name2;
|
|
} elseif (!empty($item->tax_name3)) {
|
|
$taxName = $item->tax_name3;
|
|
}
|
|
else {
|
|
$taxName = '';
|
|
}
|
|
}
|
|
|
|
$supplier_party = $this->createParty($company, $companyVatNr, $coEmail, $coPhone, $companyIdn, $coFullName, 'company', $taxName);
|
|
$ubl_invoice->setAccountingSupplierParty($supplier_party);
|
|
|
|
$customer_party = $this->createParty($client, $clientVatNr, $clEmail, $clPhone, $clientIdn, $clFullName, 'client', $taxName);
|
|
$ubl_invoice->setAccountingCustomerParty($customer_party);
|
|
|
|
$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);
|
|
|
|
// line items
|
|
$invoice_lines = [];
|
|
$taxable = $this->getTaxable();
|
|
|
|
foreach ($invoice->line_items as $index => $item) {
|
|
$invoice_lines[] = $this->createInvoiceLine($index, $item);
|
|
}
|
|
|
|
$ubl_invoice->setInvoiceLines($invoice_lines);
|
|
|
|
if (!empty($item->tax_rate1)) {
|
|
$taxRatePercent = $item->tax_rate1;
|
|
} elseif (!empty($item->tax_rate2)) {
|
|
$taxRatePercent = $item->tax_rate2;
|
|
} elseif (!empty($item->tax_rate3)) {
|
|
$taxRatePercent = $item->tax_rate3;
|
|
}else {
|
|
$taxRatePercent = 0;
|
|
}
|
|
|
|
if (!empty($item->tax_name1)) {
|
|
$taxNameScheme = $item->tax_name1;
|
|
} elseif (!empty($item->tax_name2)) {
|
|
$taxNameScheme = $item->tax_name2;
|
|
} elseif (!empty($item->tax_name3)) {
|
|
$taxNameScheme = $item->tax_name3;
|
|
} else {
|
|
$taxNameScheme = '';
|
|
}
|
|
|
|
$invoicing_data = $this->invoice->calc();
|
|
$taxtotal = new TaxTotal();
|
|
$taxtotal->setTaxAmount($invoicing_data->getItemTotalTaxes());
|
|
$taxtotal->addTaxSubTotal((new TaxSubTotal())
|
|
->setTaxAmount($invoicing_data->getItemTotalTaxes())
|
|
->setTaxableAmount($taxable)
|
|
->setTaxCategory((new TaxCategory())
|
|
->setId("S")
|
|
->setPercent($taxRatePercent)
|
|
->setTaxScheme(((new TaxScheme())->setId(($taxNameScheme === 'TVA') ? 'VAT' : $taxNameScheme)))));
|
|
|
|
$ubl_invoice->setTaxTotal($taxtotal);
|
|
|
|
$ubl_invoice->setLegalMonetaryTotal((new LegalMonetaryTotal())
|
|
->setLineExtensionAmount($taxable)
|
|
->setTaxExclusiveAmount($taxable)
|
|
->setTaxInclusiveAmount($invoice->amount)
|
|
->setPayableAmount($invoice->amount));
|
|
|
|
return $ubl_invoice;
|
|
}
|
|
|
|
private function createParty($company, $vatNr, $eMail, $phone, $idNr, $fullName, $compType, $taxNameScheme = '')
|
|
{
|
|
$party = new Party();
|
|
$party->setPartyIdentification(preg_replace('/^RO/', '', $vatNr));
|
|
$address = new Address();
|
|
if ($compType === 'company') {
|
|
$address->setCityName($this->resolveSectorCode($company->settings->state));
|
|
$address->setStreetName($company->settings->address1);
|
|
$address->setCountrySubentity($this->resolveSubEntityCode($company->settings->city));
|
|
} elseif ($compType === 'client') {
|
|
$address->setCityName($this->resolveSectorCode($company->state));
|
|
$address->setStreetName($company->address1);
|
|
$address->setCountrySubentity($this->resolveSubEntityCode($company->city));
|
|
}
|
|
|
|
if ($compType === 'company') {
|
|
if ($company->settings->country_id) {
|
|
$country = new Country();
|
|
$country->setIdentificationCode($company->country()->iso_3166_2);
|
|
$address->setCountry($country);
|
|
}
|
|
} elseif ($compType === 'client') {
|
|
if ($company->country_id) {
|
|
$country = new Country();
|
|
$country->setIdentificationCode($company->country->iso_3166_2);
|
|
$address->setCountry($country);
|
|
}
|
|
}
|
|
|
|
$party->setPostalAddress($address);
|
|
|
|
$taxScheme = null;
|
|
if (preg_match('/^RO/', $vatNr)) {
|
|
$taxScheme = (new TaxScheme())
|
|
->setCompanyId($vatNr)
|
|
->setId(($taxNameScheme === 'TVA') ? 'VAT' : $taxNameScheme);
|
|
}
|
|
|
|
$party->setTaxScheme($taxScheme);
|
|
|
|
$legalEntity = new LegalEntity();
|
|
if ($compType === 'company') {
|
|
$legalEntity->setRegistrationName($company->settings->name);
|
|
} elseif ($compType === 'client') {
|
|
$legalEntity->setRegistrationName($company->name);
|
|
}
|
|
|
|
if (preg_match('/^RO/', $vatNr)) {
|
|
$legalEntity->setCompanyId($idNr);
|
|
} else {
|
|
$legalEntity->setCompanyId($vatNr);
|
|
}
|
|
|
|
$party->setLegalEntity($legalEntity);
|
|
|
|
$contact = (new Contact())
|
|
->setName($fullName)
|
|
->setElectronicMail($eMail)
|
|
->setTelephone($phone);
|
|
|
|
$party->setContact($contact);
|
|
|
|
return $party;
|
|
}
|
|
|
|
private function createInvoiceLine($index, $item)
|
|
{
|
|
if (strlen($item->tax_name1) > 1) {
|
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
|
->setPercent($item->tax_rate1)
|
|
->setTaxScheme(((new TaxScheme())->setId(($item->tax_name1 === 'TVA') ? 'VAT' : $item->tax_name1)));
|
|
} elseif (strlen($item->tax_name2) > 1) {
|
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
|
->setPercent($item->tax_rate2)
|
|
->setTaxScheme(((new TaxScheme())->setId(($item->tax_name2 === 'TVA') ? 'VAT' : $item->tax_name2)));
|
|
} elseif (strlen($item->tax_name3) > 1) {
|
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
|
->setId($this->resolveTaxCode($item->tax_id ?? 1))
|
|
->setPercent($item->tax_rate3)
|
|
->setTaxScheme(((new TaxScheme())->setId(($item->tax_name3 === 'TVA') ? 'VAT' : $item->tax_name3)));
|
|
}else {
|
|
|
|
$classifiedTaxCategory = (new ClassifiedTaxCategory())
|
|
->setId($this->resolveTaxCode($item->tax_id ?? 8))
|
|
->setPercent(0)
|
|
->setTaxScheme(((new TaxScheme())->setId(($item->tax_name3 === 'TVA') ? 'VAT' : $item->tax_name3)));
|
|
|
|
}
|
|
|
|
$invoiceLine = (new InvoiceLine())
|
|
->setId($index + 1)
|
|
->setInvoicedQuantity($item->quantity)
|
|
->setUnitCode($item->unit_code ?? 'C62')
|
|
->setLineExtensionAmount($item->line_total)
|
|
->setItem((new Item())
|
|
->setName($item->product_key)
|
|
->setDescription($item->notes)
|
|
->setClassifiedTaxCategory([$classifiedTaxCategory]))
|
|
->setPrice((new Price())
|
|
->setPriceAmount($this->costWithDiscount($item)));
|
|
|
|
//->setSellersItemIdentification("1ABCD"));
|
|
|
|
return $invoiceLine;
|
|
}
|
|
|
|
private function createTaxRate(&$taxtotal, $taxable, $taxRate, $taxName)
|
|
{
|
|
$invoice = $this->invoice;
|
|
$taxAmount = $this->taxAmount($taxable, $taxRate);
|
|
$taxScheme = ((new TaxScheme()))->setId($taxName);
|
|
|
|
$taxtotal->addTaxSubTotal((new TaxSubTotal())
|
|
->setTaxAmount($taxAmount)
|
|
->setTaxableAmount($taxable)
|
|
->setTaxCategory((new TaxCategory())
|
|
->setId($taxName)
|
|
->setName($taxName)
|
|
->setTaxScheme($taxScheme)
|
|
->setPercent($taxRate)));
|
|
|
|
return $taxAmount;
|
|
}
|
|
|
|
/**
|
|
* @param $item
|
|
* @param $invoice_total
|
|
* @return float|int
|
|
*/
|
|
private function getItemTaxable($item, $invoice_total)
|
|
{
|
|
$total = $item->quantity * $item->cost;
|
|
|
|
if ($this->invoice->discount != 0) {
|
|
if ($this->invoice->is_amount_discount) {
|
|
/** @var float $invoice_total */
|
|
if ($invoice_total + $this->invoice->discount != 0) {
|
|
$total -= $invoice_total ? ($total / ($invoice_total + $this->invoice->discount) * $this->invoice->discount) : 0;
|
|
}
|
|
} else {
|
|
$total *= (100 - $this->invoice->discount) / 100;
|
|
}
|
|
}
|
|
|
|
if ($item->discount != 0) {
|
|
if ($this->invoice->is_amount_discount) {
|
|
$total -= $item->discount;
|
|
} else {
|
|
$total -= $total * $item->discount / 100;
|
|
}
|
|
}
|
|
|
|
return round($total, 2);
|
|
}
|
|
|
|
/**
|
|
* @return float|int|mixed
|
|
*/
|
|
private function getTaxable()
|
|
{
|
|
$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;
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
public function taxAmount($taxable, $rate)
|
|
{
|
|
if ($this->invoice->uses_inclusive_taxes) {
|
|
return round($taxable - ($taxable / (1 + ($rate / 100))), 2);
|
|
} else {
|
|
return round($taxable * ($rate / 100), 2);
|
|
}
|
|
}
|
|
|
|
private function resolveTaxCode($tax_id)
|
|
{
|
|
$code = $tax_id;
|
|
|
|
match($tax_id){
|
|
Product::PRODUCT_TYPE_REVERSE_TAX => $code = 'AE', // VAT_REVERSE_CHARGE =
|
|
Product::PRODUCT_TYPE_EXEMPT => $code = 'E', // EXEMPT_FROM_TAX =
|
|
Product::PRODUCT_TYPE_PHYSICAL => $code = 'S', // STANDARD_RATE =
|
|
Product::PRODUCT_TYPE_ZERO_RATED => $code = 'Z', // ZERO_RATED_GOODS =
|
|
Product::PRODUCT_TYPE_REDUCED_TAX => $code = 'AA', // LOWER_RATE =
|
|
Product::PRODUCT_TYPE_SERVICE => $code = 'S', // STANDARD_RATE =
|
|
Product::PRODUCT_TYPE_DIGITAL => $code = 'S', // STANDARD_RATE =
|
|
Product::PRODUCT_TYPE_SHIPPING => $code = 'S', // STANDARD_RATE =
|
|
Product::PRODUCT_TYPE_OVERRIDE_TAX => $code = 'S', // STANDARD_RATE =
|
|
default => $code = 'S',
|
|
};
|
|
|
|
return $code;
|
|
}
|
|
|
|
public function generateXml(): string
|
|
{
|
|
$ubl_invoice = $this->run(); // Call the existing handle method to get the UBLInvoice
|
|
$generator = new Generator();
|
|
return $generator->invoice($ubl_invoice, $this->invoice->client->getCurrencyCode());
|
|
}
|
|
|
|
}
|