1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-15 07:33:04 +01:00
invoiceninja/app/Services/EDocument/Standards/Peppol.php
2024-08-08 14:16:35 +10:00

1273 lines
40 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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\Company;
use App\Models\Invoice;
use App\Helpers\Invoice\Taxer;
use App\Services\AbstractService;
use App\Helpers\Invoice\InvoiceSum;
use InvoiceNinja\EInvoice\EInvoice;
use App\Utils\Traits\NumberFormatter;
use App\Helpers\Invoice\InvoiceSumInclusive;
use InvoiceNinja\EInvoice\Models\Peppol\PaymentMeans;
use InvoiceNinja\EInvoice\Models\Peppol\ItemType\Item;
use InvoiceNinja\EInvoice\Models\Peppol\PartyType\Party;
use InvoiceNinja\EInvoice\Models\Peppol\PriceType\Price;
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID;
use InvoiceNinja\EInvoice\Models\Peppol\AddressType\Address;
use InvoiceNinja\EInvoice\Models\Peppol\ContactType\Contact;
use InvoiceNinja\EInvoice\Models\Peppol\CountryType\Country;
use InvoiceNinja\EInvoice\Models\Peppol\PartyIdentification;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxAmount;
use InvoiceNinja\EInvoice\Models\Peppol\Party as PeppolParty;
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotalType\TaxTotal;
use App\Services\EDocument\Standards\Settings\PropertyResolver;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PriceAmount;
use InvoiceNinja\EInvoice\Models\Peppol\PartyNameType\PartyName;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\PayableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxableAmount;
use InvoiceNinja\EInvoice\Models\Peppol\TaxTotal as PeppolTaxTotal;
use InvoiceNinja\EInvoice\Models\Peppol\InvoiceLineType\InvoiceLine;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory;
use InvoiceNinja\EInvoice\Models\Peppol\TaxSubtotalType\TaxSubtotal;
use InvoiceNinja\EInvoice\Models\Peppol\TaxScheme as PeppolTaxScheme;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxExclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\LineExtensionAmount;
use InvoiceNinja\EInvoice\Models\Peppol\OrderReferenceType\OrderReference;
use InvoiceNinja\EInvoice\Models\Peppol\MonetaryTotalType\LegalMonetaryTotal;
use InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\ClassifiedTaxCategory;
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
use InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomerAssignedAccountID;
class Peppol extends AbstractService
{
use Taxer;
use NumberFormatter;
/**
* Assumptions:
*
* Line Item Taxes Only
* Exclusive Taxes
*
*
* used as a proxy for
* the schemeID of partyidentification
* property - for Storecove only:
*
* Used in the format key:value
*
* ie. IT:IVA / DE:VAT
*
* Note there are multiple options for the following countries:
*
* US (EIN/SSN) employer identification number / social security number
* IT (CF/IVA) Codice Fiscale (person/company identifier) / company vat number
*
* @var array
*/
private array $schemeIdIdentifiers = [
'US' => 'EIN',
'US' => 'SSN',
'NZ' => 'GST',
'CH' => 'VAT', // VAT number = CHE - 999999999 - MWST|IVA|VAT
'IS' => 'VAT',
'LI' => 'VAT',
'NO' => 'VAT',
'AD' => 'VAT',
'AL' => 'VAT',
'AT' => 'VAT',
'BA' => 'VAT',
'BE' => 'VAT',
'BG' => 'VAT',
'AU' => 'ABN', //Australia
'CA' => 'CBN', //Canada
'MX' => 'RFC', //Mexico
'NZ' => 'GST', //Nuuu zulund
'GB' => 'VAT', //Great Britain
'SA' => 'TIN', //South Africa
'CY' => 'VAT',
'CZ' => 'VAT',
'DE' => 'VAT', //tested - requires Payment Means to be defined.
'DK' => 'ERST',
'EE' => 'VAT',
'ES' => 'VAT', //tested - B2G pending
'FI' => 'VAT',
'FR' => 'VAT',
'GR' => 'VAT',
'HR' => 'VAT',
'HU' => 'VAT',
'IE' => 'VAT',
'IT' => 'IVA', //tested - Requires a Customer Party Identification (VAT number)
'IT' => 'CF', //tested - Requires a Customer Party Identification (VAT number)
'LT' => 'VAT',
'LU' => 'VAT',
'LV' => 'VAT',
'MC' => 'VAT',
'ME' => 'VAT',
'MK' => 'VAT',
'MT' => 'VAT',
'NL' => 'VAT',
'PL' => 'VAT',
'PT' => 'VAT',
'RO' => 'VAT',
'RS' => 'VAT',
'SE' => 'VAT',
'SI' => 'VAT',
'SK' => 'VAT',
'SM' => 'VAT',
'TR' => 'VAT',
'VA' => 'VAT',
'IN' => 'GSTIN',
'JP' => 'IIN',
'MY' => 'TIN',
'SG' => 'GST',
'GB' => 'VAT',
'SA' => 'TIN',
];
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 Company $company;
private InvoiceSum | InvoiceSumInclusive $calc;
private \InvoiceNinja\EInvoice\Models\Peppol\Invoice $p_invoice;
private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_client_settings;
private ?\InvoiceNinja\EInvoice\Models\Peppol\Invoice $_company_settings;
private EInvoice $e;
private array $storecove_meta = [];
/**
* @param Invoice $invoice
*/
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
$this->calc = $this->invoice->calc();
$this->e = new EInvoice();
$this->setSettings()->setInvoice();
}
/**
* Rehydrates an existing e invoice - or - scaffolds a new one
*
* @return self
*/
private function setInvoice(): self
{
if($this->invoice->e_invoice){
$this->p_invoice = $this->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;
}
/**
* Transforms the settings props into usable models we can merge.
*
* @return self
*/
private function setSettings(): self
{
$this->_client_settings = isset($this->invoice->client->e_invoice->Invoice) ? $this->e->decode('Peppol', json_encode($this->invoice->client->e_invoice->Invoice), 'json') : null;
$this->_company_settings = isset($this->invoice->company->e_invoice->Invoice) ? $this->e->decode('Peppol', json_encode($this->invoice->company->e_invoice->Invoice), 'json') : null;
return $this;
}
public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice
{
//@todo - need to process this and remove null values
return $this->p_invoice;
}
public function toXml(): string
{
$e = new EInvoice();
$xml = $e->encode($this->p_invoice, 'xml');
$prefix = '<?xml version="1.0" encoding="utf-8"?>
<Invoice
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">';
return str_ireplace(['\n','<?xml version="1.0"?>'], ['', $prefix], $xml);
}
public function toJson(): string
{
$e = new EInvoice();
$json = $e->encode($this->p_invoice, 'json');
return $json;
}
public function toArray(): array
{
return json_decode($this->toJson(), true);
}
public function run()
{
$this->p_invoice->ID = $this->invoice->number;
$this->p_invoice->IssueDate = new \DateTime($this->invoice->date);
if($this->invoice->due_date)
$this->p_invoice->DueDate = new \DateTime($this->invoice->due_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(); it only wants the aggregate here!!
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->countryLevelMutators();
return $this;
}
private function getLegalMonetaryTotal(): LegalMonetaryTotal
{
$taxable = $this->getTaxable();
$lmt = new LegalMonetaryTotal();
$lea = new LineExtensionAmount();
$lea->currencyID = $this->invoice->client->currency()->code;
$lea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $taxable;
$lmt->LineExtensionAmount = $lea;
$tea = new TaxExclusiveAmount();
$tea->currencyID = $this->invoice->client->currency()->code;
$tea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes,2) : $taxable;
$lmt->TaxExclusiveAmount = $tea;
$tia = new TaxInclusiveAmount();
$tia->currencyID = $this->invoice->client->currency()->code;
$tia->amount = $this->invoice->amount;
$lmt->TaxInclusiveAmount = $tia;
$pa = new PayableAmount();
$pa->currencyID = $this->invoice->client->currency()->code;
$pa->amount = $this->invoice->amount;
$lmt->PayableAmount = $pa;
return $lmt;
}
private function getTotalTaxAmount(): float
{
if(!$this->invoice->total_taxes)
return 0;
elseif($this->invoice->uses_inclusive_taxes)
return $this->invoice->total_taxes;
return $this->calcAmountLineTax($this->invoice->tax_rate1, $this->invoice->amount) ?? 0;
}
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 = $this->getTotalTaxAmount();
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = $type_id == '2' ? 'HUR' : 'C62';
$tc->Percent = $this->invoice->tax_rate1;
$ts = new PeppolTaxScheme();
$ts->ID = strlen($this->invoice->tax_name1 ?? '') > 1 ? $this->invoice->tax_name1 : '0';
$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 = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($this->invoice->tax_rate2, $this->invoice->amount) : $this->calcAmountLineTax($this->invoice->tax_rate2, $this->invoice->amount);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount- $this->invoice->total_taxes : $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_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 = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($this->invoice->tax_rate3, $this->invoice->amount) : $this->calcAmountLineTax($this->invoice->tax_rate3, $this->invoice->amount);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->invoice->amount - $this->invoice->total_taxes : $this->invoice->amount;
$tax_subtotal->TaxableAmount = $taxable_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;
$lea = new LineExtensionAmount();
$lea->currencyID = $this->invoice->client->currency()->code;
// $lea->amount = $item->line_total;
$lea->amount = $this->invoice->uses_inclusive_taxes ? $item->line_total - $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) : $item->line_total;
$line->LineExtensionAmount = $lea;
$line->Item = $_item;
$item_taxes = $this->getItemTaxes($item);
if(count($item_taxes) > 0) {
$line->TaxTotal = $item_taxes;
}
// else {
// $line->TaxTotal = $this->zeroTaxAmount();
// }
$price = new Price();
$pa = new PriceAmount();
$pa->currencyID = $this->invoice->client->currency()->code;
$pa->amount = $this->costWithDiscount($item) - ( $this->invoice->uses_inclusive_taxes ? ($this->calcInclusiveLineTax($item->tax_rate1, $item->line_total)/$item->quantity) : 0);
$price->PriceAmount = $pa;
$line->Price = $price;
$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 zeroTaxAmount(): array
{
$blank_tax = [];
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = '0';
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = '0';
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
$tc->ID = 'Z';
$tc->Percent = 0;
$ts = new PeppolTaxScheme();
$ts->ID = '0';
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$blank_tax[] = $tax_total;
return $blank_tax;
}
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 = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate1, $item->line_total) : $this->calcAmountLineTax($item->tax_rate1, $item->line_total);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $item->line_total - $tax_amount->amount : $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_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;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
if(strlen($item->tax_name2 ?? '') > 1) {
$tax_amount = new TaxAmount();
$tax_amount->currencyID = $this->invoice->client->currency()->code;
$tax_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate2, $item->line_total) : $this->calcAmountLineTax($item->tax_rate2, $item->line_total);
$tax_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_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;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
if(strlen($item->tax_name3 ?? '') > 1) {
$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_subtotal = new TaxSubtotal();
$tax_subtotal->TaxAmount = $tax_amount;
$taxable_amount = new TaxableAmount();
$taxable_amount->currencyID = $this->invoice->client->currency()->code;
$taxable_amount->amount = $item->line_total;
$tax_subtotal->TaxableAmount = $taxable_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;
$tax_total = new TaxTotal();
$tax_total->TaxAmount = $tax_amount;
$tax_total->TaxSubtotal[] = $tax_subtotal;
$item_taxes[] = $tax_total;
}
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;
$address->CountrySubentity = $this->invoice->company->settings->state;
// $address->CountrySubentityCode = $this->invoice->company->settings->state;
$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->getSetting('Invoice.AccountingSupplierParty.Party.Contact') ?? $this->invoice->company->owner()->present()->email();
$contact->Telephone = $this->getSetting('Invoice.AccountingSupplierParty.Party.Telephone') ?? $this->invoice->company->getSetting('phone');
$contact->Name = $this->getSetting('Invoice.AccountingSupplierParty.Party.Name') ?? $this->invoice->company->owner()->present()->name();
$party->Contact = $contact;
$asp->Party = $party;
return $asp;
}
private function getAccountingCustomerParty(): AccountingCustomerParty
{
$acp = new AccountingCustomerParty();
$party = new Party();
if(strlen($this->invoice->client->vat_number ?? '') > 1) {
$pi = new PartyIdentification;
$vatID = new ID;
$vatID->schemeID = 'CH:MWST';
$vatID->value = $this->invoice->client->vat_number;
$pi->ID = $vatID;
$party->PartyIdentification[] = $pi;
}
$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;
$address->CountrySubentity = $this->invoice->client->state;
// $address->CountrySubentityCode = $this->invoice->client->state;
$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;
}
///////////////// Helper Methods /////////////////////////
/**
* setInvoiceDefaults
*
* Stubs a default einvoice
* @return self
*/
public function setInvoiceDefaults(): self
{
$settings = [
'AccountingCostCode' => 7,
'AccountingCost' => 7,
'BuyerReference' => 6,
'AccountingSupplierParty' => 1,
'AccountingCustomerParty' => 2,
'PayeeParty' => 1,
'BuyerCustomerParty' => 2,
'SellerSupplierParty' => 1,
'TaxRepresentativeParty' => 1,
'Delivery' => 1,
'DeliveryTerms' => 7,
'PaymentMeans' => 7,
'PaymentTerms' => 7,
];
//only scans for top level props
foreach($settings as $prop => $visibility){
if($prop_value = $this->getSetting($prop))
$this->p_invoice->{$prop} = $prop_value;
}
return $this;
}
/**
* getSetting
*
* Attempts to harvest and return a preconfigured prop from company / client / invoice settings
*
* @param string $property_path
* @return mixed
*/
public function getSetting(string $property_path): mixed
{
if($prop_value = PropertyResolver::resolve($this->p_invoice, $property_path)) {
return $prop_value;
}elseif($prop_value = PropertyResolver::resolve($this->_client_settings, $property_path)) {
return $prop_value;
}elseif($prop_value = PropertyResolver::resolve($this->_company_settings, $property_path)) {
return $prop_value;
}
return null;
}
/**
* countryLevelMutators
*
* Runs country level specific requirements for the e-invoice
* @return self
*/
private function countryLevelMutators():self
{
if(method_exists($this, $this->invoice->company->country()->iso_3166_2))
$this->{$this->invoice->company->country()->iso_3166_2}();
return $this;
}
/**
* setPaymentMeans
*
* Sets the payment means - if it exists
* @param bool $required
* @return self
*/
private function setPaymentMeans(bool $required = false): self
{
if(isset($this->p_invoice->PaymentMeans))
return $this;
elseif($paymentMeans = $this->getSetting('Invoice.PaymentMeans')){
$this->p_invoice->PaymentMeans = is_array($paymentMeans) ? $paymentMeans : [$paymentMeans];
return $this;
}
return $this->checkRequired($required, "Payment Means");
}
/**
* setOrderReference
*
* sets the order reference - if it exists (Never rely on settings for this)
*
* @param bool $required
* @return self
*/
private function setOrderReference(bool $required = false): self
{
$this->p_invoice->BuyerReference = $this->invoice->po_number ?? '';
if(strlen($this->invoice->po_number ?? '') > 1)
{
$order_reference = new OrderReference();
$id = new ID();
$id->value = $this->invoice->po_number;
$order_reference->ID = $id;
$this->p_invoice->OrderReference = $order_reference;
$this->setStorecoveMeta(["document" => [
"invoice" => [
"references" => [
"documentType" => "purchase_order",
"documentId" => $this->invoice->po_number,
],
],
]
]);
return $this;
}
return $this->checkRequired($required, 'Order Reference');
}
/**
* setCustomerAssignedAccountId
*
* Sets the client id_number CAN rely on settings
*
* @param bool $required
* @return self
*/
private function setCustomerAssignedAccountId(bool $required = false): self
{
//@phpstan-ignore-next-line
if(isset($this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID)){
return $this;
}
elseif($customer_assigned_account_id = $this->getSetting('Invoice.AccountingCustomerParty.CustomerAssignedAccountID')){
$this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id;
return $this;
}
elseif(strlen($this->invoice->client->id_number ?? '') > 1){
$customer_assigned_account_id = new CustomerAssignedAccountID();
$customer_assigned_account_id->value = $this->invoice->client->id_number;
$this->p_invoice->AccountingCustomerParty->CustomerAssignedAccountID = $customer_assigned_account_id;
return $this;
}
//@phpstan-ignore-next-line
return $this->checkRequired($required, 'Client ID Number');
}
/**
* Check Required
*
* Throws if a required field is missing.
*
* @param bool $required
* @param string $section
* @return self
*/
private function checkRequired(bool $required, string $section): self
{
return $required ? throw new \Exception("e-invoice generation halted:: {$section} required") : $this;
}
/**
* Builds the Routing object for StoreCove
*
* @param string $schemeId
* @param string $id
* @return array
*/
private function buildRouting(string $schemeId, string $id): array
{
return
[
"routing" => [
"publicIdentifiers" => [
[
"scheme" => $schemeId,
"id" => $id
]
]
]
];
}
////////////////////////// Country level mutators /////////////////////////////////////
/**
* DE
*
* @Completed
* @Tested
*
* @return self
*/
private function DE(): self
{
$this->setPaymentMeans(true);
return $this;
}
/**
* setStorecoveMeta
*
* updates the storecove payload for sending documents
*
* @param array $meta
* @return self
*/
private function setStorecoveMeta(array $meta): self
{
$this->storecove_meta = array_merge($this->storecove_meta, $meta);
return $this;
}
/**
* CH
*
* @Completed
*
* Completed - QR-Bill to be implemented at a later date.
* @return self
*/
private function CH(): self
{
return $this;
}
/**
* AT
*
* @Pending
*
* Need to ensure when sending to government entities that we route appropriately
* Also need to ensure customerAssignedAccountIdValue is set so that the sender can be resolved.
*
* Need a way to define if the client is a government entity.
*
* @return self
*/
private function AT(): self
{
//special fields for sending to AT:GOV
return $this;
}
private function AU(): self
{
//if payment means are included, they must be the same `type`
return $this;
}
/**
* ES
*
* @Pending
* B2G configuration
* B2G Testing
*
* testing. // routing identifier - 293098
*
* @return self
*/
private function ES(): self
{
if(!isset($this->invoice->due_date))
$this->p_invoice->DueDate = new \DateTime($this->invoice->date);
if($this->invoice->client->classification == 'business' && $this->invoice->company->getSetting('classification') == 'business') {
//must have a paymentmeans as credit_transfer
$this->setPaymentMeans(true);
}
// For B2G, provide three ES:FACE identifiers in the routing object,
// as well as the ES:VAT tax identifier in the accountingCustomerParty.publicIdentifiers.
// The invoice will then be routed through the FACe network. The three required ES:FACE identifiers are as follows:
// "routing": {
// "eIdentifiers":[
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-01-FISCAL"
// },
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-02-RECEPTOR"
// },
// {
// "scheme": "ES:FACE",
// "id": "L01234567",
// "role": "ES-03-PAGADOR"
// }
// ]
// }
return $this;
}
private function FI(): self
{
// For Finvoice, provide an FI:OPID routing identifier and an FI:OVT legal identifier.
// An FI:VAT is recommended. In many cases (depending on the sender/receiver country and the type of service/goods)
// an FI:VAT is required. So we recommend always including this.
return $this;
}
/**
* FR
* @Pending - clarification on codes needed
*
* @return self
*/
private function FR(): self
{
// When sending invoices to the French government (Chorus Pro):
// All invoices have to be routed to SIRET 0009:11000201100044. There is no test environment for sending to public entities.
// The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array.
if($this->invoice->client->classification == 'government'){
//route to SIRET 0009:11000201100044
$this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:11000201100044"));
// The SIRET / 0009 identifier of the final recipient is to be included in the invoice.accountingCustomerParty.publicIdentifiers array.
$this->setCustomerAssignedAccountId(true);
}
if(strlen($this->invoice->client->id_number ?? '') == 9) {
//SIREN
$this->setStorecoveMeta($this->buildRouting('FR:SIREN', "0002:{$this->invoice->client->id_number}"));
}
else {
//SIRET
$this->setStorecoveMeta($this->buildRouting('FR:SIRET', "0009:{$this->invoice->client->id_number}"));
}
// ??????????????????????? //@TODO
// The service code must be sent in invoice.buyerReference (deprecated) or the invoice.references array (documentType buyer_reference)
if(strlen($this->invoice->po_number ?? '') >1) {
$this->setOrderReference(false);
}
return $this;
}
private function IT(): self
{
// IT Sender, IT Receiver, B2B/B2G
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
// IT Sender, IT Receiver, B2C
// Provide the receiver IT:CF and the receiver IT:CUUO (codice destinatario)
// IT Sender, non-IT Receiver
// Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers).
// non-IT Sender, IT Receiver, B2B/B2G
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
// non-IT Sender, IT Receiver, B2C
// Provide the receiver IT:CF and an optional email. The invoice will be eReported and sent via email. Note that this cannot be a PEC email address.
return $this;
}
private function MY(): self
{
//way too much to digest here, delayed.
return $this;
}
private function NL(): self
{
// When sending to public entities, the invoice.accountingSupplierParty.party.contact.email is mandatory.
// Dutch senders and receivers require a legal identifier. For companies, this is NL:KVK, for public entities this is NL:OINO.
return $this;
}
private function NZ(): self
{
// New Zealand uses a GLN to identify businesses. In addition, when sending invoices to a New Zealand customer, make sure you include the pseudo identifier NZ:GST as their tax identifier.
return $this;
}
private function PL(): self
{
// Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "PL:VAT",
// "id": "PL0101010101"
// }
// ],
// "networks": [
// {
// "application": "pl-ksef",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Note this will only work if your LegalEntity has been setup for this network.
return $this;
}
private function RO(): self
{
// Because using this network is not yet mandatory, the default workflow is to not use this network. Therefore, you have to force its use, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "RO:VAT",
// "id": "RO010101010"
// }
// ],
// "networks": [
// {
// "application": "ro-anaf",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Note this will only work if your LegalEntity has been setup for this network.
// The county field for a Romania address must use the ISO3166-2:RO codes, e.g. "RO-AB, RO-AR". Dont omit the country prefix!
// The city field for county RO-B must be SECTOR1 - SECTOR6.
return $this;
}
private function SG(): self
{
//delayed - stage 2
return $this;
}
//Sweden
private function SE(): self
{
// Deliver invoices to the "Svefaktura" co-operation of local Swedish service providers.
// Routing is through the SE:ORGNR together with a network specification:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "SE:ORGNR",
// "id": "0012345678"
// }
// ],
// "networks": [
// {
// "application": "svefaktura",
// "settings": {
// "enabled": true
// }
// }
// ]
// }
// Use of the "Svefaktura" co-operation can also be induced by specifying an operator id, as follows:
// "routing": {
// "eIdentifiers": [
// {
// "scheme": "SE:ORGNR",
// "id": "0012345678"
// },
// {
// "scheme": "SE:OPID",
// "id": "1234567890"
// }
// ]
// }
return $this;
}
}