mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-23 20:00:33 -04:00
1770 lines
55 KiB
PHP
1770 lines
55 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\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 App\Services\EDocument\Standards\Peppol\RO;
|
|
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 App\Services\EDocument\Gateway\Storecove\StorecoveRouter;
|
|
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\AmountType\TaxExclusiveAmount;
|
|
use InvoiceNinja\EInvoice\Models\Peppol\AmountType\TaxInclusiveAmount;
|
|
use InvoiceNinja\EInvoice\Models\Peppol\LocationType\PhysicalLocation;
|
|
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\IdentifierType\CustomerAssignedAccountID;
|
|
use InvoiceNinja\EInvoice\Models\Peppol\CustomerPartyType\AccountingCustomerParty;
|
|
use InvoiceNinja\EInvoice\Models\Peppol\SupplierPartyType\AccountingSupplierParty;
|
|
use InvoiceNinja\EInvoice\Models\Peppol\FinancialAccountType\PayeeFinancialAccount;
|
|
|
|
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', //Tested - Routing GOV + Business
|
|
'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', //tested - Need to ensure Siren/Siret routing
|
|
'GR' => 'VAT',
|
|
'HR' => 'VAT',
|
|
'HU' => 'VAT',
|
|
'IE' => 'VAT',
|
|
'IT' => 'IVA', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' Cannot test currently
|
|
'IT' => 'CF', //tested - Requires a Customer Party Identification (VAT number) - 'IT senders must first be provisioned in the partner system.' Cannot test currently
|
|
'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"
|
|
];
|
|
|
|
// 0 1 2 3
|
|
// ["Country" => ["B2X","Legal","Tax","Routing"],
|
|
private array $routing_rules = [
|
|
"US" => [
|
|
["B","DUNS, GLN, LEI","US:EIN","DUNS, GLN, LEI"],
|
|
// ["B","DUNS, GLN, LEI","US:SSN","DUNS, GLN, LEI"],
|
|
],
|
|
"CA" => ["B","CA:CBN",false,"CA:CBN"],
|
|
"MX" => ["B","MX:RFC",false,"MX:RFC"],
|
|
"AU" => ["B+G","AU:ABN",false,"AU:ABN"],
|
|
"NZ" => ["B+G","GLN","NZ:GST","GLN"],
|
|
"CH" => ["B+G","CH:UIDB","CH:VAT","CH:UIDB"],
|
|
"IS" => ["B+G","IS:KTNR","IS:VAT","IS:KTNR"],
|
|
"LI" => ["B+G","","LI:VAT","LI:VAT"],
|
|
"NO" => ["B+G","NO:ORG","NO:VAT","NO:ORG"],
|
|
"AD" => ["B+G","","AD:VAT","AD:VAT"],
|
|
"AL" => ["B+G","","AL:VAT","AL:VAT"],
|
|
"AT" => [
|
|
["G","AT:GOV",false,"9915:b"],
|
|
["B","","AT:VAT","AT:VAT"],
|
|
],
|
|
"BA" => ["B+G","","BA:VAT","BA:VAT"],
|
|
"BE" => ["B+G","BE:EN","BE:VAT","BE:EN"],
|
|
"BG" => ["B+G","","BG:VAT","BG:VAT"],
|
|
"CY" => ["B+G","","CY:VAT","CY:VAT"],
|
|
"CZ" => ["B+G","","CZ:VAT","CZ:VAT"],
|
|
"DE" => [
|
|
["G","DE:LWID",false,"DE:LWID"],
|
|
["B","","DE:VAT","DE:VAT"],
|
|
],
|
|
"DK" => ["B+G","DK:DIGST","DK:ERST","DK:DIGST"],
|
|
"EE" => ["B+G","EE:CC","EE:VAT","EE:CC"],
|
|
"ES" => ["B","","ES:VAT","ES:VAT"],
|
|
"FI" => ["B+G","FI:OVT","FI:VAT","FI:OVT"],
|
|
"FR" => [
|
|
["G","FR:SIRET + customerAssignedAccountIdValue",false,"0009:11000201100044"],
|
|
["B","FR:SIRENE or FR:SIRET","FR:VAT","FR:SIRENE or FR:SIRET"],
|
|
],
|
|
"GR" => ["B+G","","GR:VAT","GR:VAT"],
|
|
"HR" => ["B+G","","HR:VAT","HR:VAT"],
|
|
"HU" => ["B+G","","HU:VAT","HU:VAT"],
|
|
"IE" => ["B+G","","IE:VAT","IE:VAT"],
|
|
"IT" => [
|
|
["G","","IT:IVA","IT:CUUO"], // (Peppol)
|
|
["B","","IT:IVA","IT:CUUO"], // (SDI)
|
|
// ["B","","IT:CF","IT:CUUO"], // (SDI)
|
|
["C","","IT:CF","Email"],// (SDI)
|
|
["G","","IT:IVA","IT:CUUO"],// (SDI)
|
|
],
|
|
"LT" => ["B+G","LT:LEC","LT:VAT","LT:LEC"],
|
|
"LU" => ["B+G","LU:MAT","LU:VAT","LU:VAT"],
|
|
"LV" => ["B+G","","LV:VAT","LV:VAT"],
|
|
"MC" => ["B+G","","MC:VAT","MC:VAT"],
|
|
"ME" => ["B+G","","ME:VAT","ME:VAT"],
|
|
"MK" => ["B+G","","MK:VAT","MK:VAT"],
|
|
"MT" => ["B+G","","MT:VAT","MT:VAT"],
|
|
"NL" => ["G","NL:OINO",false,"NL:OINO"],
|
|
"NL" => ["B","NL:KVK","NL:VAT","NL:KVK or NL:VAT"],
|
|
"PL" => ["G+B","","PL:VAT","PL:VAT"],
|
|
"PT" => ["G+B","","PT:VAT","PT:VAT"],
|
|
"RO" => ["G+B","","RO:VAT","RO:VAT"],
|
|
"RS" => ["G+B","","RS:VAT","RS:VAT"],
|
|
"SE" => ["G+B","SE:ORGNR","SE:VAT","SE:ORGNR"],
|
|
"SI" => ["G+B","","SI:VAT","SI:VAT"],
|
|
"SK" => ["G+B","","SK:VAT","SK:VAT"],
|
|
"SM" => ["G+B","","SM:VAT","SM:VAT"],
|
|
"TR" => ["G+B","","TR:VAT","TR:VAT"],
|
|
"VA" => ["G+B","","VA:VAT","VA:VAT"],
|
|
"IN" => ["B","","IN:GSTIN","Email"],
|
|
"JP" => ["B","JP:SST","JP:IIN","JP:SST"],
|
|
"MY" => ["B","MY:EIF","MY:TIN","MY:EIF"],
|
|
"SG" => [
|
|
["G","SG:UEN",false,"0195:SGUENT08GA0028A"],
|
|
["B","SG:UEN","SG:GST","SG:UEN"],
|
|
],
|
|
"GB" => ["B","","GB:VAT","GB:VAT"],
|
|
"SA" => ["B","","SA:TIN","Email"],
|
|
"Other" => ["B","DUNS, GLN, LEI",false,"DUNS, GLN, LEI"],
|
|
];
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* Entry point for building document
|
|
*
|
|
* @return self
|
|
*/
|
|
public function run(): self
|
|
{
|
|
$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 = ($this->invoice->amount >= 0) ? 380 : 381; //
|
|
$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->senderSpecificLevelMutators()
|
|
->receiverSpecificLevelMutators();
|
|
|
|
if(strlen($this->invoice->backup ?? '') == 0)
|
|
{
|
|
$this->invoice->e_invoice = $this->toObject();
|
|
$this->invoice->save();
|
|
}
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
}
|
|
|
|
/**
|
|
* getInvoice
|
|
*
|
|
* @return \InvoiceNinja\EInvoice\Models\Peppol\Invoice
|
|
*/
|
|
public function getInvoice(): \InvoiceNinja\EInvoice\Models\Peppol\Invoice
|
|
{
|
|
//@todo - need to process this and remove null values
|
|
return $this->p_invoice;
|
|
|
|
}
|
|
|
|
/**
|
|
* toXml
|
|
*
|
|
* @return string
|
|
*/
|
|
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);
|
|
|
|
}
|
|
|
|
/**
|
|
* toJson
|
|
*
|
|
* @return string
|
|
*/
|
|
public function toJson(): string
|
|
{
|
|
$e = new EInvoice();
|
|
$json = $e->encode($this->p_invoice, 'json');
|
|
|
|
return $json;
|
|
|
|
}
|
|
|
|
/**
|
|
* toObject
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function toObject(): mixed
|
|
{
|
|
|
|
$invoice = new \stdClass;
|
|
$invoice->Invoice = json_decode($this->toJson());
|
|
return $invoice;
|
|
|
|
}
|
|
|
|
/**
|
|
* toArray
|
|
*
|
|
* @return array
|
|
*/
|
|
public function toArray(): array
|
|
{
|
|
return ['Invoice' => json_decode($this->toJson(), true)];
|
|
}
|
|
|
|
/**
|
|
* getLegalMonetaryTotal
|
|
*
|
|
* @return LegalMonetaryTotal
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* getTotalTaxAmount
|
|
*
|
|
* @return float
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* getTotalTaxes
|
|
*
|
|
* @return array
|
|
*/
|
|
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();
|
|
$id = new ID();
|
|
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
|
$tc->ID = $id;
|
|
$tc->Percent = $this->invoice->tax_rate1;
|
|
$ts = new TaxScheme();
|
|
$id = new ID();
|
|
$id->value = strlen($this->invoice->tax_name1 ?? '') > 1 ? $this->invoice->tax_name1 : '0';
|
|
$ts->ID = $id;
|
|
$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();
|
|
$id = new ID();
|
|
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
|
$tc->ID = $id;
|
|
$tc->Percent = $this->invoice->tax_rate2;
|
|
$ts = new TaxScheme();
|
|
$id = new ID();
|
|
$id->value = $this->invoice->tax_name2;
|
|
$ts->ID = $id;
|
|
$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();
|
|
|
|
$id = new ID();
|
|
$id->value = $type_id == '2' ? 'HUR' : 'C62';
|
|
$tc->ID = $id;
|
|
$tc->Percent = $this->invoice->tax_rate3;
|
|
$ts = new TaxScheme();
|
|
|
|
$id = new ID();
|
|
$id->value = $this->invoice->tax_name3;
|
|
|
|
$ts->ID = $id;
|
|
$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();
|
|
|
|
$id = new ID();
|
|
$id->value = (string) ($key+1);
|
|
$line->ID = $id;
|
|
$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 = (string) ($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;
|
|
}
|
|
|
|
/**
|
|
* costWithDiscount
|
|
*
|
|
* @param mixed $item
|
|
* @return float
|
|
*/
|
|
private function costWithDiscount($item): float
|
|
{
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* zeroTaxAmount
|
|
*
|
|
* @return array
|
|
*/
|
|
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();
|
|
$id = new ID();
|
|
$id->value = 'Z';
|
|
$tc->ID = $id;
|
|
$tc->Percent = '0';
|
|
$ts = new TaxScheme();
|
|
|
|
$id = new ID();
|
|
$id->value = '0';
|
|
$ts->ID = $id;
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* getItemTaxes
|
|
*
|
|
* @param object $item
|
|
* @return array
|
|
*/
|
|
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();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
|
|
|
$tc->ID = $id;
|
|
$tc->Percent = $item->tax_rate1;
|
|
$ts = new TaxScheme();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->tax_name1;
|
|
|
|
$ts->ID = $id;
|
|
$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();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
|
|
|
$tc->ID = $id;
|
|
$tc->Percent = $item->tax_rate2;
|
|
$ts = new TaxScheme();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->tax_name2;
|
|
|
|
$ts->ID = $id;
|
|
$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();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->type_id == '2' ? 'HUR' : 'C62';
|
|
|
|
$tc->ID = $id;
|
|
$tc->Percent = $item->tax_rate3;
|
|
$ts = new TaxScheme();
|
|
|
|
$id = new ID();
|
|
$id->value = $item->tax_name3;
|
|
|
|
$ts->ID = $id;
|
|
$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;
|
|
}
|
|
|
|
/**
|
|
* getAccountingSupplierParty
|
|
*
|
|
* @return AccountingSupplierParty
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* resolveTaxScheme
|
|
*
|
|
* @return string
|
|
*/
|
|
private function resolveTaxScheme(): string
|
|
{
|
|
return (new StorecoveRouter())->resolveTaxScheme($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
|
|
|
|
// $rules = isset($this->routing_rules[$this->invoice->client->country->iso_3166_2]) ? $this->routing_rules[$this->invoice->client->country->iso_3166_2] : [false, false, false, false,];
|
|
|
|
// $code = false;
|
|
|
|
// match($this->invoice->client->classification) {
|
|
// "business" => $code = "B",
|
|
// "government" => $code = "G",
|
|
// "individual" => $code = "C",
|
|
// default => $code = false,
|
|
// };
|
|
|
|
// //single array
|
|
// if(is_array($rules) && !is_array($rules[0])) {
|
|
// return $rules[2];
|
|
// }
|
|
|
|
// foreach($rules as $rule) {
|
|
// if(stripos($rule[0], $code) !== false) {
|
|
// return $rule[2];
|
|
// }
|
|
// }
|
|
|
|
// return false;
|
|
}
|
|
|
|
/**
|
|
* getAccountingCustomerParty
|
|
*
|
|
* @return AccountingCustomerParty
|
|
*/
|
|
private function getAccountingCustomerParty(): AccountingCustomerParty
|
|
{
|
|
|
|
$acp = new AccountingCustomerParty();
|
|
|
|
$party = new Party();
|
|
|
|
if(strlen($this->invoice->client->vat_number ?? '') > 1) {
|
|
|
|
$pi = new PartyIdentification();
|
|
|
|
$vatID = new ID();
|
|
|
|
if($scheme = $this->resolveTaxScheme()) {
|
|
$vatID->schemeID = $scheme;
|
|
}
|
|
|
|
$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;
|
|
|
|
$physical_location = new PhysicalLocation();
|
|
$physical_location->Address = $address;
|
|
|
|
$party->PhysicalLocation = $physical_location;
|
|
;
|
|
|
|
$contact = new Contact();
|
|
$contact->ElectronicMail = $this->invoice->client->present()->email();
|
|
|
|
$party->Contact = $contact;
|
|
|
|
$acp->Party = $party;
|
|
|
|
return $acp;
|
|
}
|
|
|
|
/**
|
|
* getTaxable
|
|
*
|
|
* @return float
|
|
*/
|
|
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 /////////////////////////
|
|
|
|
/**
|
|
* getClientRoutingCode
|
|
*
|
|
* @return string
|
|
*/
|
|
private function getClientRoutingCode(): string
|
|
{
|
|
// $receiver_identifiers = $this->routing_rules[$this->invoice->client->country->iso_3166_2];
|
|
// $client_classification = $this->invoice->client->classification == 'government' ? 'G' : 'B';
|
|
|
|
// if(count($receiver_identifiers) > 1) {
|
|
|
|
// foreach($receiver_identifiers as $ident) {
|
|
// if(str_contains($ident[0], $client_classification)) {
|
|
// return $ident[3];
|
|
// }
|
|
// }
|
|
|
|
// } elseif(count($receiver_identifiers) == 1) {
|
|
// return $receiver_identifiers[3];
|
|
// }
|
|
|
|
return (new StorecoveRouter())->resolveRouting($this->invoice->client->country->iso_3166_2, $this->invoice->client->classification);
|
|
|
|
// throw new \Exception("e-invoice generation halted:: Could not resolve the Tax Code for this client? {$this->invoice->client->hashed_id}");
|
|
|
|
}
|
|
|
|
/**
|
|
* setInvoiceDefaults
|
|
*
|
|
* Stubs a default einvoice
|
|
* @return self
|
|
*/
|
|
public function setInvoiceDefaults(): self
|
|
{
|
|
|
|
// Stub new invoice with company settings.
|
|
if($this->_company_settings)
|
|
{
|
|
foreach(get_object_vars($this->_company_settings) as $prop => $value){
|
|
$this->p_invoice->{$prop} = $value;
|
|
}
|
|
}
|
|
|
|
// Overwrite with any client level settings
|
|
if($this->_client_settings)
|
|
{
|
|
foreach (get_object_vars($this->_client_settings) as $prop => $value) {
|
|
$this->p_invoice->{$prop} = $value;
|
|
}
|
|
}
|
|
|
|
// Plucks special overriding properties scanning the correct settings level
|
|
$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;
|
|
|
|
}
|
|
|
|
/**
|
|
* getClientSetting
|
|
*
|
|
* @param string $property_path
|
|
* @return mixed
|
|
*/
|
|
private function getClientSetting(string $property_path): mixed
|
|
{
|
|
return PropertyResolver::resolve($this->_client_settings, $property_path);
|
|
}
|
|
|
|
/**
|
|
* getCompanySetting
|
|
*
|
|
* @param string $property_path
|
|
* @return mixed
|
|
*/
|
|
private function getCompanySetting(string $property_path): mixed
|
|
{
|
|
return PropertyResolver::resolve($this->_company_settings, $property_path);
|
|
}
|
|
|
|
/**
|
|
* senderSpecificLevelMutators
|
|
*
|
|
* Runs sender level specific requirements for the e-invoice,
|
|
*
|
|
* ie, mutations that are required by the senders country.
|
|
*
|
|
* @return self
|
|
*/
|
|
private function senderSpecificLevelMutators(): self
|
|
{
|
|
|
|
if(method_exists($this, $this->invoice->company->country()->iso_3166_2)) {
|
|
$this->{$this->invoice->company->country()->iso_3166_2}();
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* receiverSpecificLevelMutators
|
|
*
|
|
* Runs receiver level specific requirements for the e-invoice
|
|
*
|
|
* ie mutations that are required by the receiving country
|
|
* @return self
|
|
*/
|
|
private function receiverSpecificLevelMutators(): self
|
|
{
|
|
|
|
if(method_exists($this, "client_{$this->invoice->company->country()->iso_3166_2}")) {
|
|
$this->{"client_{$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 array $identifiers
|
|
* @return array
|
|
*/
|
|
private function buildRouting(array $identifiers): array
|
|
{
|
|
return
|
|
[
|
|
"routing" => [
|
|
"eIdentifiers" =>
|
|
$identifiers,
|
|
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* setEmailRouting
|
|
*
|
|
* @param string $email
|
|
* @return self
|
|
*/
|
|
private function setEmailRouting(string $email): self
|
|
{
|
|
nlog($email);
|
|
|
|
$meta = $this->getStorecoveMeta();
|
|
|
|
if(isset($meta['routing']['emails'])) {
|
|
$emails = $meta['routing']['emails'];
|
|
array_push($emails, $email);
|
|
$meta['routing']['emails'] = $emails;
|
|
} else {
|
|
$meta['routing']['emails'] = [$email];
|
|
}
|
|
|
|
$this->setStorecoveMeta($meta);
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* getStorecoveMeta
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getStorecoveMeta(): array
|
|
{
|
|
return $this->storecove_meta;
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////// Country level mutators /////////////////////////////////////
|
|
|
|
/**
|
|
* DE
|
|
*
|
|
* @Completed
|
|
* @Tested
|
|
*
|
|
* @return self
|
|
*/
|
|
private function DE(): self
|
|
{
|
|
|
|
$this->setPaymentMeans(true);
|
|
|
|
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
|
|
|
|
if($this->invoice->client->classification == 'government') {
|
|
//routing "b" for production "test" for test environment
|
|
$this->setStorecoveMeta($this->buildRouting(["scheme" => 'AT:GOV', "id" => 'b']));
|
|
|
|
//for government clients this must be set.
|
|
$this->setCustomerAssignedAccountId(true);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* FI
|
|
*
|
|
* @return self
|
|
*/
|
|
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([
|
|
["scheme" => 'FR:SIRET', "id" => '11000201100044']
|
|
|
|
// ["scheme" => 'FR:SIRET', "id" => '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([
|
|
["scheme" => 'FR:SIRET', "id" => "{$this->invoice->client->id_number}"]
|
|
|
|
// ["scheme" => 'FR:SIRET', "id" => "0002:{$this->invoice->client->id_number}"]
|
|
]));
|
|
} else {
|
|
//SIRET
|
|
$this->setStorecoveMeta($this->buildRouting([
|
|
["scheme" => 'FR:SIRET', "id" => "{$this->invoice->client->id_number}"]
|
|
|
|
// ["scheme" => 'FR:SIRET', "id" => "0009:{$this->invoice->client->id_number}"]
|
|
]));
|
|
}
|
|
|
|
// Apparently this is not a special field according to support
|
|
// sounds like it is optional
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* IT
|
|
*
|
|
* @return self
|
|
*/
|
|
private function IT(): self
|
|
{
|
|
|
|
// IT Sender, IT Receiver, B2B/B2G
|
|
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
|
|
if(in_array($this->invoice->client->classification, ['business','government']) && $this->invoice->company->country()->iso_3166_2 == 'IT') {
|
|
|
|
$this->setStorecoveMeta($this->buildRouting([
|
|
["scheme" => 'IT:IVA', "id" => $this->invoice->client->vat_number],
|
|
["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id]
|
|
]));
|
|
|
|
return $this;
|
|
}
|
|
|
|
// IT Sender, IT Receiver, B2C
|
|
// Provide the receiver IT:CF and the receiver IT:CUUO (codice destinatario)
|
|
if($this->invoice->client->classification == 'individual' && $this->invoice->company->country()->iso_3166_2 == 'IT') {
|
|
|
|
$this->setStorecoveMeta($this->buildRouting([
|
|
["scheme" => 'IT:CF', "id" => $this->invoice->client->vat_number],
|
|
// ["scheme" => 'IT:CUUO', "id" => $this->invoice->client->routing_id]
|
|
]));
|
|
|
|
$this->setEmailRouting($this->invoice->client->present()->email());
|
|
|
|
return $this;
|
|
}
|
|
|
|
// IT Sender, non-IT Receiver
|
|
// Provide the receiver tax identifier and any routing identifier applicable to the receiving country (see Receiver Identifiers).
|
|
if($this->invoice->client->country->iso_3166_2 != 'IT' && $this->invoice->company->country()->iso_3166_2 == 'IT') {
|
|
|
|
$code = $this->getClientRoutingCode();
|
|
|
|
nlog("foreign receiver");
|
|
$this->setStorecoveMeta($this->buildRouting([
|
|
["scheme" => $code, "id" => $this->invoice->client->vat_number]
|
|
]));
|
|
|
|
return $this;
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* client_IT
|
|
*
|
|
* @return self
|
|
*/
|
|
private function client_IT(): self
|
|
{
|
|
|
|
// 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.
|
|
if(in_array($this->invoice->client->classification, ['individual']) && $this->invoice->company->country()->iso_3166_2 != 'IT') {
|
|
|
|
return $this;
|
|
}
|
|
|
|
// non-IT Sender, IT Receiver, B2B/B2G
|
|
// Provide the receiver IT:VAT and the receiver IT:CUUO (codice destinatario)
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
/**
|
|
* MY
|
|
*
|
|
* @return self
|
|
*/
|
|
private function MY(): self
|
|
{
|
|
//way too much to digest here, delayed.
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* NL
|
|
*
|
|
* @return self
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* NZ
|
|
*
|
|
* @return self
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* PL
|
|
*
|
|
* @return self
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* RO
|
|
*
|
|
* @return self
|
|
*/
|
|
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:
|
|
$meta = ["networks" => [
|
|
[
|
|
"application" => "ro-anaf",
|
|
"settings" => [
|
|
"enabled" => true
|
|
],
|
|
],
|
|
]];
|
|
|
|
$this->setStorecoveMeta($meta);
|
|
|
|
$this->setStorecoveMeta($this->buildRouting([
|
|
["scheme" => 'RO:VAT', "id" => $this->invoice->client->vat_number],
|
|
]));
|
|
|
|
$ro = new RO($this->invoice);
|
|
|
|
$client_state = $this->getClientSetting('Invoice.AccountingSupplierParty.Party.PostalAddress.Address.CountrySubentity');
|
|
$client_city = $this->getClientSetting('Invoice.AccountingCustomerParty.Party.PostalAddress.Address.CityName');
|
|
|
|
$resolved_state = $ro->getStateCode($client_state);
|
|
$resolved_city = $ro->getSectorCode($client_city);
|
|
|
|
$this->p_invoice->AccountingCustomerParty->Party->PostalAddress->CountrySubentity = $resolved_state;
|
|
$this->p_invoice->AccountingCustomerParty->Party->PostalAddress->CityName = $resolved_city;
|
|
$this->p_invoice->AccountingCustomerParty->Party->PhysicalLocation->Address->CountrySubentity = $resolved_state;
|
|
$this->p_invoice->AccountingCustomerParty->Party->PhysicalLocation->Address->CityName = $resolved_city;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* SG
|
|
*
|
|
* @return self
|
|
*/
|
|
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;
|
|
}
|
|
}
|