Refactoring storecove

This commit is contained in:
David Bomba 2024-11-04 19:16:04 +11:00
parent de1c7fdc0e
commit 59ec545c1c
13 changed files with 1363 additions and 221 deletions

View File

@ -237,38 +237,64 @@ class Rule extends BaseRule implements RuleInterface
*/
public function calculateRates(): self
{
// Tax exempt clients always get zero tax
if ($this->client->is_tax_exempt) {
// nlog("tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) {
// nlog("euro zone and tax exempt");
return $this;
}
// B2B within EU with valid VAT
if ($this->client_subregion != $this->client->company->tax_data->seller_subregion &&
in_array($this->client_subregion, $this->eu_country_codes) &&
$this->client->vat_number &&
$this->client->has_valid_vat_number &&
$this->eu_business_tax_exempt) {
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes) && ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt)) { //foreign + tax exempt
// nlog("foreign and tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold");
return $this;
}
// Non-EU transactions
if (!in_array($this->client_subregion, $this->eu_country_codes)) {
if ($this->foreign_consumer_tax_exempt || $this->foreign_business_tax_exempt) {
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} else {
$this->defaultForeign();
}
return $this;
}
// B2C or invalid VAT within EU
$is_b2c = strlen($this->client->vat_number ?? '') <= 1 ||
!$this->client->has_valid_vat_number ||
$this->client->classification == 'individual';
if ($is_b2c) {
$is_over_threshold = $this->client->company->tax_data->regions->EU->has_sales_above_threshold ?? false;
if ($is_over_threshold && $this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// Over threshold - use destination country rates
$this->tax_name = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_name;
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;
} else {
// nlog("EU with intra-community supply ie DE to DE");
// Under threshold or domestic - use origin country rates
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
}
} else {
// nlog("default tax");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
return $this;
}
// Default case (B2B without valid VAT)
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate ?? 0;
return $this;
}
}

View File

@ -141,8 +141,8 @@ class InvoiceItemSum
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency();
if ($this->client) {
$this->currency = $this->client->currency();
$this->shouldCalculateTax();
} else {
$this->currency = $this->invoice->vendor->currency();
@ -158,7 +158,7 @@ class InvoiceItemSum
return $this;
}
$this->calcLineItems();
$this->calcLineItems()->getPeppolSurchargeTaxes();
return $this;
}
@ -186,7 +186,6 @@ class InvoiceItemSum
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
/** @var \App\DataMapper\Tax\BaseRule $class */
$class = "App\DataMapper\Tax\\".str_replace("-","_",$this->client->company->country()->iso_3166_2)."\\Rule";
@ -208,6 +207,46 @@ class InvoiceItemSum
return $this;
}
private function calculateNexus()
{
$company_country_code = $this->invoice->company->country()->iso_3166_2;
$client_country_code = $this->client->country->iso_3166_2;
$base_rule = new \App\DataMapper\Tax\BaseRule();
$eu_countries = $base_rule->eu_country_codes;
$nexus_rule = $company_country_code;
if ($client_country_code == $company_country_code) {
//Domestic Sales
$nexus_rule = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && !in_array($client_country_code, $eu_countries)) {
//NON-EU Sale
$nexus_rule = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && in_array($client_country_code, $eu_countries)) {
//EU Sale
// Invalid VAT number = seller country nexus
if(isset($this->client->has_valid_vat_number) && !$this->client->has_valid_vat_number){
$nexus_rule = $company_country_code;
}
elseif (isset($this->invoice->company->tax_data->regions->EU->has_sales_above_threshold) && $this->invoice->company->tax_data->regions->EU->has_sales_above_threshold) { //over threshold - tax in buyer country
$nexus_rule = $client_country_code;
} elseif (isset($this->invoice->company->tax_data->regions->EU->has_sales_above_threshold) && !$this->invoice->company->tax_data->regions->EU->has_sales_above_threshold) {
$nexus_rule = $company_country_code;
} elseif ($this->client->classification != 'individual' && (isset($this->client->has_valid_vat_number) && !$this->client->has_valid_vat_number)){
$nexus_rule = $company_country_code;
} else {
$nexus_rule = $company_country_code;
}
}
nlog($nexus_rule);
$class = "App\DataMapper\Tax\\".str_replace("-", "_", $nexus_rule)."\\Rule";
return $class;
}
private function push(): self
{
$this->sub_total += round($this->getLineTotal(), $this->currency->precision);
@ -324,6 +363,50 @@ class InvoiceItemSum
return $this;
}
private function getPeppolSurchargeTaxes(): self
{
if(!$this->client->getSetting('e_invoice_type') == 'PEPPOL')
return $this;
collect($this->invoice->line_items)
->flatMap(function ($item) {
return collect([1, 2, 3])
->map(fn ($i) => [
'name' => $item->{"tax_name{$i}"} ?? '',
'percentage' => $item->{"tax_rate{$i}"} ?? 0,
])
->filter(fn ($tax) => strlen($tax['name']) > 1);
})
->unique(fn ($tax) => $tax['percentage'] . '_' . $tax['name'])
->values()
->each(function ($tax){
$tax_component = 0;
if ($this->invoice->custom_surcharge1) {
$tax_component += round($this->invoice->custom_surcharge1 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge2) {
$tax_component += round($this->invoice->custom_surcharge2 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge3) {
$tax_component += round($this->invoice->custom_surcharge3 * ($tax['percentage'] / 100), 2);
}
if ($this->invoice->custom_surcharge4) {
$tax_component += round($this->invoice->custom_surcharge4 * ($tax['percentage'] / 100), 2);
}
$this->groupTax($tax['name'], $tax['percentage'], $tax_component);
});
return $this;
}
private function groupTax($tax_name, $tax_rate, $tax_total)
{
$group_tax = [];

View File

@ -240,15 +240,11 @@ class InvoiceSum
public function getRecurringInvoice()
{
// $this->invoice->amount = $this->formatValue($this->getTotal(), $this->precision);
// $this->invoice->total_taxes = $this->getTotalTaxes();
$this->setCalculatedAttributes();
$this->invoice->balance = $this->invoice->amount;
$this->invoice->saveQuietly();
// $this->invoice->saveQuietly();
return $this->invoice;
}
@ -426,4 +422,14 @@ class InvoiceSum
return $this;
}
public function getNetSubtotal()
{
return $this->getSubTotal() - $this->getTotalDiscount();
}
public function getSubtotalWithSurcharges()
{
return $this->getSubTotal() + $this->getTotalSurcharges();
}
}

View File

@ -401,6 +401,11 @@ class InvoiceSumInclusive
return $this->getTotalTaxes();
}
public function getNetSubtotal()
{
return $this->getSubTotal() - $this->getTotalDiscount();
}
public function purgeTaxes()
{
return $this;

View File

@ -77,6 +77,7 @@ class Product extends BaseModel
public const PRODUCT_TYPE_OVERRIDE_TAX = 7;
public const PRODUCT_TYPE_ZERO_RATED = 8;
public const PRODUCT_TYPE_REVERSE_TAX = 9;
public const PRODUCT_INTRA_COMMUNITY = 10;
protected $fillable = [
'custom_value1',

View File

@ -51,4 +51,10 @@ class AccountingSupplierParty
$this->public_identifiers = $public_identifiers;
return $this;
}
public function addPublicIdentifiers($public_identifier): self
{
$this->public_identifiers[] = $public_identifier;
return $this;
}
}

View File

@ -7,20 +7,20 @@ use Symfony\Component\Serializer\Attribute\SerializedPath;
class AllowanceCharges
{
#[SerializedPath('[cbc:Amount][#]')]
public ?string $amount_excluding_vat;
// #[SerializedPath('[cbc:Amount][#]')]
public ?float $amount_excluding_vat;
// #[SerializedPath('[cbc:BaseAmount][#]')]
public ?string $amount_excluding_tax;
#[SerializedPath('[cbc:Amount][#]')]
public ?float $amount_excluding_tax;
#[SerializedPath('[cbc:BaseAmount][#]')]
public ?string $base_amount_excluding_tax;
public ?float $base_amount_excluding_tax;
#[SerializedPath('[cbc:Amount][@currencyID]')]
public ?string $amount_including_tax;
// #[SerializedPath('[cbc:Amount][#]')]
public ?float $amount_including_tax;
#[SerializedPath('[cbc:BaseAmount][@currencyID]')]
public ?string $base_amount_including_tax;
// #[SerializedPath('[cbc:BaseAmount][#]')]
public ?float $base_amount_including_tax;
// #[SerializedPath('[cac:TaxCategory]')]
// public ?Tax $tax;
@ -39,11 +39,11 @@ class AllowanceCharges
* @param TaxesDutiesFees[] $taxes_duties_fees
*/
public function __construct(
?string $amount_excluding_vat,
?string $amount_excluding_tax,
?string $base_amount_excluding_tax,
?string $amount_including_tax,
?string $base_amount_including_tax,
?float $amount_excluding_vat,
?float $amount_excluding_tax,
?float $base_amount_excluding_tax,
?float $amount_including_tax,
?float $base_amount_including_tax,
// ?Tax $tax,
?array $taxes_duties_fees,
?string $reason,
@ -60,27 +60,27 @@ class AllowanceCharges
$this->reason_code = $reason_code;
}
public function getAmountExcludingVat(): ?string
public function getAmountExcludingVat(): ?float
{
return $this->amount_excluding_vat;
}
public function getAmountExcludingTax(): ?string
public function getAmountExcludingTax(): ?float
{
return $this->amount_excluding_tax;
}
public function getBaseAmountExcludingTax(): ?string
public function getBaseAmountExcludingTax(): ?float
{
return $this->base_amount_excluding_tax;
}
public function getAmountIncludingTax(): ?string
public function getAmountIncludingTax(): ?float
{
return $this->amount_including_tax;
}
public function getBaseAmountIncludingTax(): ?string
public function getBaseAmountIncludingTax(): ?float
{
return $this->base_amount_including_tax;
}
@ -103,31 +103,31 @@ class AllowanceCharges
return $this->reason_code;
}
public function setAmountExcludingVat(?string $amount_excluding_vat): self
public function setAmountExcludingVat(?float $amount_excluding_vat): self
{
$this->amount_excluding_vat = $amount_excluding_vat;
return $this;
}
public function setAmountExcludingTax(?string $amount_excluding_tax): self
public function setAmountExcludingTax(?float $amount_excluding_tax): self
{
$this->amount_excluding_tax = $amount_excluding_tax;
return $this;
}
public function setBaseAmountExcludingTax(?string $base_amount_excluding_tax): self
public function setBaseAmountExcludingTax(?float $base_amount_excluding_tax): self
{
$this->base_amount_excluding_tax = $base_amount_excluding_tax;
return $this;
}
public function setAmountIncludingTax(?string $amount_including_tax): self
public function setAmountIncludingTax(?float $amount_including_tax): self
{
$this->amount_including_tax = $amount_including_tax;
return $this;
}
public function setBaseAmountIncludingTax(?string $base_amount_including_tax): self
public function setBaseAmountIncludingTax(?float $base_amount_including_tax): self
{
$this->base_amount_including_tax = $base_amount_including_tax;
return $this;

View File

@ -38,12 +38,12 @@ class InvoiceLines
#[SerializedPath('[cac:AllowanceCharge]')]
/** @var AllowanceCharges[] */ //todo
public ?array $charges;
public ?array $allowance_charges;
#[SerializedPath('[cbc:LineExtensionAmount][#]')]
public ?float $amount_excluding_vat;
#[SerializedPath('[cbc:TaxExclusiveAmount][#]')]
#[SerializedPath('[cac:Price][cbc:PriceAmount][value]')]
public ?float $amount_excluding_tax;
#[SerializedPath('[cbc:TaxInclusiveAmount][#]')]
@ -51,7 +51,7 @@ class InvoiceLines
#[SerializedPath('[cac:Item][cac:ClassifiedTaxCategory]')]
/** @var TaxesDutiesFees[] */
public array $taxes_duties_fees;
public ?array $taxes_duties_fees = [];
#[SerializedPath('[cbc:AccountingCost]')]
public ?string $accounting_cost;
@ -83,7 +83,7 @@ class InvoiceLines
public ?string $note;
/**
* @param AllowanceCharges[] $charges
* @param AllowanceCharges[] $allowance_charges
* @param TaxesDutiesFees[] $taxes_duties_fees
* @param References[] $references
* @param AdditionalItemProperties[] $additional_item_properties
@ -98,7 +98,7 @@ class InvoiceLines
?float $quantity,
?float $base_quantity,
?string $quantity_unit_code,
?array $charges,
?array $allowance_charges,
?float $amount_excluding_vat,
?float $amount_excluding_tax,
?float $amount_including_tax,
@ -122,7 +122,7 @@ class InvoiceLines
$this->quantity = $quantity;
$this->base_quantity = $base_quantity;
$this->quantity_unit_code = $quantity_unit_code;
$this->charges = $charges;
$this->allowance_charges = $allowance_charges;
$this->amount_excluding_vat = $amount_excluding_vat;
$this->amount_excluding_tax = $amount_excluding_tax;
$this->amount_including_tax = $amount_including_tax;
@ -188,7 +188,7 @@ class InvoiceLines
*/
public function getAllowanceCharges(): ?array
{
return $this->charges;
return $this->allowance_charges;
}
public function getAmountExcludingVat(): ?float
@ -320,11 +320,11 @@ class InvoiceLines
}
/**
* @param AllowanceCharges[] $charges
* @param AllowanceCharges[] $allowance_charges
*/
public function setAllowanceCharges(?array $charges): self
public function setAllowanceCharges(?array $allowance_charges): self
{
$this->charges = $charges;
$this->allowance_charges = $allowance_charges;
return $this;
}

View File

@ -42,6 +42,36 @@ class StorecoveAdapter
private string $nexus;
public function validate(): self
{
return $this;
}
public function getInvoice(): Invoice
{
return $this->storecove_invoice;
}
public function getErrors(): array
{
return $this->errors;
}
/**
* addError
*
* Adds an error to the errors array.
*
* @param string $error
* @return self
*/
private function addError(string $error): self
{
$this->errors[] = $error;
return $this;
}
/**
* transform
*
@ -54,32 +84,30 @@ class StorecoveAdapter
$serializer = $this->getSerializer();
/** Currently - due to class structures, the serialization process goes like this:
*
* e-invoice => Peppol -> XML -> Peppol Decoded -> encode to Peppol -> deserialize to Storecove
*/
$p = (new Peppol($invoice))->run()->toXml();
nlog($p);
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$context = [
DateTimeNormalizer::FORMAT_KEY => 'Y-m-d',
AbstractObjectNormalizer::SKIP_NULL_VALUES => true,
];
$e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml');
nlog($peppolInvoice);
$parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);
$e = new \InvoiceNinja\EInvoice\EInvoice();
$peppolInvoice = $e->decode('Peppol', $p, 'xml');
$parent = \App\Services\EDocument\Gateway\Storecove\Models\Invoice::class;
$peppolInvoice = $data = $e->encode($peppolInvoice, 'json');
$this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'json', $context);
// $s_invoice = $serializer->encode($invoice, 'json', $context);
// $arr = json_decode($s_invoice, true);
// $data = $this->removeEmptyValues($arr);
nlog($this->storecove_invoice);
$this->buildNexus();
// @phpstan-ignore-next-line
// $this->storecove_invoice = $serializer->deserialize($data, Invoice::class, 'json', $context);
return $this;
}
@ -89,16 +117,39 @@ $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'js
//set all taxmap countries - resolve the taxing country
$lines = $this->storecove_invoice->getInvoiceLines();
foreach($lines as $line)
foreach($lines as &$line)
{
foreach($line->taxes_duties_fees as &$tax)
if(isset($line->taxes_duties_fees))
{
$tax->country = $this->nexus;
if(property_exists($tax,'category'))
$tax->category = $this->tranformTaxCode($tax->category);
foreach($line->taxes_duties_fees as &$tax)
{
$tax->country = $this->nexus;
if(property_exists($tax,'category'))
$tax->category = $this->tranformTaxCode($tax->category);
}
unset($tax);
}
if(isset($line->allowance_charges))
{
foreach($line->allowance_charges as &$allowance)
{
if($allowance->reason == ctrans('texts.discount'))
$allowance->amount_excluding_tax = $allowance->amount_excluding_tax * -1;
foreach($allowance->getTaxesDutiesFees as &$tax)
{
if (property_exists($tax, 'category')) {
$tax->category = $this->tranformTaxCode($tax->category);
}
}
unset($tax);
}
unset($allowance);
}
unset($tax);
}
$this->storecove_invoice->setInvoiceLines($lines);
@ -128,43 +179,46 @@ $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'js
$this->storecove_invoice->setPaymentMeansArray($payment_means);
$allowances = $this->storecove_invoice->getAllowanceCharges() ?? [];
foreach($allowances as &$allowance)
{
$taxes = $allowance->getTaxesDutiesFees() ?? [];
foreach($taxes as &$tax)
{
$tax->country = $this->nexus;
if (property_exists($tax, 'category')) {
$tax->category = $this->tranformTaxCode($tax->category);
}
}
unset($tax);
if ($allowance->reason == ctrans('texts.discount')) {
$allowance->amount_excluding_tax = $allowance->amount_excluding_tax * -1;
}
$allowance->setTaxesDutiesFees($taxes);
}
unset($allowance);
$this->storecove_invoice->setAllowanceCharges($allowances);
$this->storecove_invoice->setTaxSystem('tax_line_percentages');
//set additional identifier if required (ie de => FR with FR vat)
return $this;
}
public function validate(): self
{
// $this->valid_document
return $this;
}
public function getInvoice(): Invoice
{
return $this->storecove_invoice;
}
public function getErrors(): array
{
return $this->errors;
}
private function addError(string $error): self
{
$this->errors[] = $error;
return $this;
}
private function getSerializer()
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// list of PropertyListExtractorInterface (any iterable)
$typeExtractors = [$reflectionExtractor,$phpDocExtractor];
// list of PropertyDescriptionExtractorInterface (any iterable)
$descriptionExtractors = [$phpDocExtractor];
// list of PropertyAccessExtractorInterface (any iterable)
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
$propertyInitializableExtractors,
@ -185,7 +239,12 @@ $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'js
return $serializer;
}
/**
* Builds the document and appends an errors prop
*
* @return array
*/
public function getDocument(): mixed
{
$serializer = $this->getSerializer();
@ -209,7 +268,13 @@ $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'js
return $data;
}
/**
* RemoveEmptyValues
*
* @param array $array
* @return array
*/
private function removeEmptyValues(array $array): array
{
foreach ($array as $key => $value) {
@ -236,27 +301,74 @@ $this->storecove_invoice = $serializer->deserialize($peppolInvoice, $parent, 'js
$eu_countries = $br->eu_country_codes;
if ($client_country_code == $company_country_code) {
//Domestic Sales
//Domestic Sales
nlog("domestic sales");
$this->nexus = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && !in_array($client_country_code, $eu_countries)) {
//NON-EU Sale
nlog("non eu");
$this->nexus = $company_country_code;
} elseif (in_array($company_country_code, $eu_countries) && in_array($client_country_code, $eu_countries)) {
//EU Sale
// Invalid VAT number = seller country nexus
if(!$this->ninja_invoice->client->has_valid_vat_number)
$this->nexus = $company_country_code;
else if ($this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold && isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number)) { //over threshold - tax in buyer country
$this->nexus = $client_country_code;
}
// First, determine if we're over threshold
$is_over_threshold = isset($this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold) &&
$this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold;
// Is this B2B or B2C?
$is_b2c = strlen($this->ninja_invoice->client->vat_number) < 2 ||
!($this->ninja_invoice->client->has_valid_vat_number ?? false) ||
$this->ninja_invoice->client->classification == 'individual';
if (strlen($this->ninja_invoice->company->settings->vat_number) < 2) {
// No VAT registration at all - must charge origin country VAT
nlog("no company vat");
$this->nexus = $company_country_code;
} elseif ($is_b2c) {
if ($is_over_threshold) {
// B2C over threshold - need destination VAT number
if (!isset($this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number)) {
$this->addError("Tax Nexus is client country ({$client_country_code}) - however VAT number not present for this region. Document not sent!");
return $this;
}
nlog("B2C");
$this->nexus = $client_country_code;
$this->setupDestinationVAT($client_country_code);
} else {
nlog("under threshold origina country");
// B2C under threshold - origin country VAT
$this->nexus = $company_country_code;
}
} else {
nlog("B2B with valid vat");
// B2B with valid VAT - origin country
$this->nexus = $company_country_code;
}
nlog("nexus = {$this->nexus}");
nlog($this->ninja_invoice->company->tax_data->regions->EU->has_sales_above_threshold);
nlog("is b2c {$is_b2c}");
nlog("is over threshold {$is_over_threshold}");
//If we reach here? We are in an invalid state!
$this->nexus = $company_country_code;
$this->addError("Tax Nexus is client country ({$client_country_code}) - however VAT number not present for this region. Document not sent!");
}
return $this;
}
private function setupDestinationVAT($client_country_code):self
{
nlog("configuring destination tax");
$this->storecove_invoice->setConsumerTaxMode(true);
$id = $this->ninja_invoice->company->tax_data->regions->EU->subregions->{$client_country_code}->vat_number;
$scheme = $this->storecove->router->resolveTaxScheme($client_country_code, $this->ninja_invoice->client->classification ?? 'individual');
$pi = new \App\Services\EDocument\Gateway\Storecove\Models\PublicIdentifiers($scheme, $id);
$asp = $this->storecove_invoice->getAccountingSupplierParty();
$asp->addPublicIdentifiers($pi);
$this->storecove_invoice->setAccountingSupplierParty($asp);
return $this;
}

View File

@ -73,7 +73,7 @@ class StorecoveRouter
"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"],
"NL" => ["B","NL:KVK","NL:VAT","NL:VAT"],
"PL" => ["G+B","","PL:VAT","PL:VAT"],
"PT" => ["G+B","","PT:VAT","PT:VAT"],
"RO" => ["G+B","","RO:VAT","RO:VAT"],

View File

@ -48,9 +48,12 @@ class SendEDocument implements ShouldQueue
public function handle(Storecove $storecove)
{
MultiDB::setDB($this->db);
nlog("trying");
$model = $this->entity::find($this->id);
/** Concrete implementation current linked to Storecove only */
$p = new Peppol($model);
$p->run();
$identifiers = $p->gateway->mutator->setClientRoutingCode()->getStorecoveMeta();
@ -58,6 +61,7 @@ class SendEDocument implements ShouldQueue
$result = $storecove->build($model);
if (count($result['errors']) > 0) {
nlog($result);
return $result['errors'];
}
@ -70,12 +74,10 @@ class SendEDocument implements ShouldQueue
],
'tenant_id' => $model->company->company_key,
'routing' => $identifiers['routing'],
// 'e_invoicing_token' => $model->company->e_invoicing_token,
// include whitelabel key.
];
/** Concrete implementation current linked to Storecove only */
//temp
//@testing only
$sc = new \App\Services\EDocument\Gateway\Storecove\Storecove();
$r = $sc->sendJsonDocument($payload);

View File

@ -149,6 +149,10 @@ class Peppol extends AbstractService
private array $tax_map = [];
private float $allowance_total = 0;
private $globalTaxCategories;
public function __construct(public Invoice $invoice)
{
$this->company = $invoice->company;
@ -166,6 +170,7 @@ class Peppol extends AbstractService
public function run(): self
{
$this->getJurisdiction();
$this->getAllUsedTaxes();
/** Invoice Level Props */
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CustomizationID();
@ -208,8 +213,8 @@ class Peppol extends AbstractService
$this->p_invoice->AccountingSupplierParty = $this->getAccountingSupplierParty();
$this->p_invoice->AccountingCustomerParty = $this->getAccountingCustomerParty();
$this->p_invoice->InvoiceLine = $this->getInvoiceLines();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->p_invoice->AllowanceCharge = $this->getAllowanceCharges();
$this->p_invoice->LegalMonetaryTotal = $this->getLegalMonetaryTotal();
$this->setOrderReference()->setTaxBreakdown();
@ -430,18 +435,22 @@ class Peppol extends AbstractService
$allowanceCharge->ChargeIndicator = 'false'; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->calc->getTotalDiscount();
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->Amount->amount = (string)number_format($this->calc->getTotalDiscount(),2);
// Add percentage if available
if ($this->invoice->discount > 0 && !$this->invoice->is_amount_discount) {
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) number_format($this->calc->getSubtotalWithSurcharges(), 2);
$mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric();
$mfn->value = (string) ($this->invoice->discount / 100);
$mfn->value = (string)number_format(round(($this->invoice->discount), 2), 2); // Format to always show 2 decimals
$allowanceCharge->MultiplierFactorNumeric = $mfn; // Convert percentage to decimal
}
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.discount');
$allowances[] = $allowanceCharge;
}
@ -450,14 +459,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge1;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge1);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -466,14 +478,18 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge2;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge2);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -482,14 +498,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge3;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge3);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -498,14 +517,17 @@ class Peppol extends AbstractService
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
// $allowanceCharge->ChargeIndicator = true; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->invoice->custom_surcharge4;
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubTotal();
$allowanceCharge->BaseAmount->amount = (string) $this->calc->getSubtotalWithSurcharges();
$this->calculateTaxMap($this->invoice->custom_surcharge4);
$allowanceCharge->TaxCategory = $this->globalTaxCategories;
$allowanceCharge->AllowanceChargeReason = ctrans('texts.surcharge');
$allowances[] = $allowanceCharge;
}
@ -527,7 +549,7 @@ class Peppol extends AbstractService
$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;
$lea->amount = $this->invoice->uses_inclusive_taxes ? round($this->invoice->amount - $this->invoice->total_taxes, 2) : $this->calc->getSubTotal();
$lmt->LineExtensionAmount = $lea;
$tea = new TaxExclusiveAmount();
@ -545,6 +567,11 @@ class Peppol extends AbstractService
$pa->amount = $this->invoice->amount;
$lmt->PayableAmount = $pa;
$am = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\AllowanceTotalAmount();
$am->currencyID = $this->invoice->client->currency()->code;
$am->amount = (string)$this->calc->getTotalDiscount();
$lmt->AllowanceTotalAmount = $am;
return $lmt;
}
@ -576,6 +603,8 @@ class Peppol extends AbstractService
break;
case Product::PRODUCT_TYPE_REVERSE_TAX:
$tax_type = 'AE';
case Product::PRODUCT_INTRA_COMMUNITY:
$tax_type = 'K';
break;
}
@ -616,32 +645,43 @@ class Peppol extends AbstractService
$lines = [];
foreach($this->invoice->line_items as $key => $item) {
$base_price_amount = (string)$this->calculateAdjustedBaseAmount($item);
$_item = new Item();
$_item->Name = $item->product_key;
$_item->Description = $item->notes;
if($item->tax_rate1 > 0)
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = (string)$item->tax_rate1;
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name1);
$ts->ID = $id;
$ctc->TaxScheme = $ts;
if(floatval($item->tax_rate1) === 0.0)
{
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate1;
$ctc->ID->value = 'K';
$ts = new TaxScheme();
$id = new ID();
$id->value = $this->standardizeTaxSchemeId($item->tax_name1);
$ts->ID = $id;
$ctc->TaxScheme = $ts;
$_item->ClassifiedTaxCategory[] = $ctc;
$terc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\TaxExemptionReasonCode();
$terc->value = 'VATEX-EU-IC';
$ctc->TaxExemptionReasonCode = $terc;
$ctc->TaxExemptionReason = 'Intra-Community supply';
}
$_item->ClassifiedTaxCategory[] = $ctc;
if ($item->tax_rate2 > 0) {
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate2;
$ctc->Percent = (string)$item->tax_rate2;
$ts = new TaxScheme();
$id = new ID();
@ -656,7 +696,7 @@ class Peppol extends AbstractService
$ctc = new ClassifiedTaxCategory();
$ctc->ID = new ID();
$ctc->ID->value = $this->getTaxType($item->tax_id);
$ctc->Percent = $item->tax_rate3;
$ctc->Percent = (string)$item->tax_rate3;
$ts = new TaxScheme();
$id = new ID();
@ -689,11 +729,12 @@ class Peppol extends AbstractService
// Handle Price and Discounts
if ($item->discount > 0) {
// Base Price (before discount)
$basePrice = new Price();
$basePriceAmount = new PriceAmount();
$basePriceAmount->currencyID = $this->invoice->client->currency()->code;
$basePriceAmount->amount = (string)($item->cost - $this->calculateDiscountAmount($item));
$basePriceAmount->amount = (string)$item->cost;
$basePrice->PriceAmount = $basePriceAmount;
// Add Allowance Charge to Price
@ -701,29 +742,35 @@ class Peppol extends AbstractService
$allowanceCharge->ChargeIndicator = 'false'; // false = discount
$allowanceCharge->Amount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\Amount();
$allowanceCharge->Amount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->Amount->amount = (string)$this->calculateDiscountAmount($item);
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string)$item->cost;
$allowanceCharge->Amount->amount = (string)number_format($this->calculateTotalItemDiscountAmount($item),2);
$this->allowance_total += $this->calculateTotalItemDiscountAmount($item);
// Add percentage if available
if ($item->discount > 0 && !$item->is_amount_discount) {
$allowanceCharge->BaseAmount = new \InvoiceNinja\EInvoice\Models\Peppol\AmountType\BaseAmount();
$allowanceCharge->BaseAmount->currencyID = $this->invoice->client->currency()->code;
$allowanceCharge->BaseAmount->amount = (string)round(($item->cost * $item->quantity),2);
$mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric();
$mfn->value = (string) ($item->discount / 100);
$mfn->value = (string) round($item->discount,2);
$allowanceCharge->MultiplierFactorNumeric = $mfn; // Convert percentage to decimal
}
// }
// Required reason
$allowanceCharge->AllowanceChargeReason = ctrans('texts.discount');
$basePrice->AllowanceCharge[] = $allowanceCharge;
$line->Price = $basePrice;
$line->AllowanceCharge[] = $allowanceCharge;
} else {
// No discount case
$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));
$pa->amount = (string)$item->cost;
$price->PriceAmount = $pa;
$line->Price = $price;
}
@ -735,47 +782,127 @@ class Peppol extends AbstractService
}
/**
* calculateDiscountAmount
*
* Helper method to determine the discount amount to be used.
*
* @param mixed $item
* @return float
*/
private function calculateDiscountAmount($item): float
{
if ($item->is_amount_discount) {
return $item->discount / $item->quantity; // Per unit discount amount
}
// /**
// * calculateDiscountAmount
// *
// * Helper method to determine the discount amount to be used.
// *
// * @param mixed $item
// * @return float
// */
// private function calculateDiscountAmount($item): float
// {
// if ($item->is_amount_discount) {
// return $item->discount / $item->quantity; // Per unit discount amount
// }
return ($item->cost / $item->quantity) * ($item->discount / 100);
}
// return ($item->cost / $item->quantity) * ($item->discount / 100);
// }
/**
* costWithDiscount
*
* Helper method to determine the cost INCLUDING discount
*
* @param mixed $item
* @return float
*/
private function costWithDiscount($item): float
private function calculateTotalItemDiscountAmount($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;
}
if ($item->is_amount_discount) {
return $item->discount;
}
return $cost;
return ($item->cost) * ($item->discount / 100);
}
// /**
// * costWithDiscount
// *
// * Helper method to determine the cost INCLUDING discount
// *
// * @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;
// }
/**
* calculateTaxMap
*
* Generates a standard tax_map entry for a given $amount
*
* Iterates through all of the globalTaxCategories found in the document
*
* @param float $amount
* @return self
*/
private function calculateTaxMap($amount): self
{
foreach($this->globalTaxCategories as $tc)
{
$this->tax_map[] = [
'taxableAmount' => $amount,
'taxAmount' => $amount * ($tc->Percent/100),
'percentage' => $tc->Percent,
];
}
return $this;
}
/**
* getAllUsedTaxes
*
* Build a full tax category property based on all
* of the item taxes that have been applied to the invoice.
*
* @return self
*/
private function getAllUsedTaxes(): self
{
$this->globalTaxCategories = [];
collect($this->invoice->line_items)
->flatMap(function ($item) {
return collect([1, 2, 3])
->map(fn ($i) => [
'name' => $item->{"tax_name{$i}"} ?? '',
'percentage' => $item->{"tax_rate{$i}"} ?? 0,
'scheme' => $this->getTaxType($item->tax_id),
])
->filter(fn ($tax) => strlen($tax['name']) > 1);
})
->unique(fn ($tax) => $tax['percentage'] . '_' . $tax['name'])
->values()
->each(function ($tax){
$taxCategory = new \InvoiceNinja\EInvoice\Models\Peppol\TaxCategoryType\TaxCategory();
$taxCategory->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxCategory->ID->value = $tax['scheme'];
$taxCategory->Percent = (string)$tax['percentage'];
$taxScheme = new \InvoiceNinja\EInvoice\Models\Peppol\TaxSchemeType\TaxScheme();
$taxScheme->ID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$taxScheme->ID->value = $this->standardizeTaxSchemeId($tax['name']);
$taxCategory->TaxScheme = $taxScheme;
$this->globalTaxCategories[] = $taxCategory;
});
return $this;
}
/**
* getItemTaxes
*
@ -788,18 +915,20 @@ class Peppol extends AbstractService
private function getItemTaxes(object $item): array
{
$item_taxes = [];
$adjusted_base_amount = $this->calculateAdjustedBaseAmount($item);
if(strlen($item->tax_name1 ?? '') > 1) {
// 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_amount->amount = $this->invoice->uses_inclusive_taxes ? $this->calcInclusiveLineTax($item->tax_rate1, $adjusted_base_amount) : $this->calcAmountLineTax($item->tax_rate1, $adjusted_base_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 ? $item->line_total - $tax_amount->amount : $item->line_total;
$taxable_amount->amount = $this->invoice->uses_inclusive_taxes ? $adjusted_base_amount - $tax_amount->amount : $adjusted_base_amount;
$tax_subtotal->TaxableAmount = $taxable_amount;
$tc = new TaxCategory();
@ -807,8 +936,11 @@ class Peppol extends AbstractService
$id = new ID();
$id->value = $this->getTaxType($item->tax_id);
if(floatval($item->tax_rate1) === 0.0)
$id->value = 'K';
$tc->ID = $id;
$tc->Percent = $item->tax_rate1;
$tc->Percent = (string)$item->tax_rate1;
$ts = new TaxScheme();
$id = new ID();
@ -833,7 +965,7 @@ class Peppol extends AbstractService
$item_taxes[] = $tax_total;
}
// }
if(strlen($item->tax_name2 ?? '') > 1) {
@ -855,7 +987,7 @@ class Peppol extends AbstractService
$id->value = $this->getTaxType($item->tax_id);
$tc->ID = $id;
$tc->Percent = $item->tax_rate2;
$tc->Percent = (string)$item->tax_rate2;
$ts = new TaxScheme();
$id = new ID();
@ -904,7 +1036,7 @@ class Peppol extends AbstractService
$id->value = $this->getTaxType($item->tax_id);
$tc->ID = $id;
$tc->Percent = $item->tax_rate3;
$tc->Percent = (string)$item->tax_rate3;
$ts = new TaxScheme();
$id = new ID();
@ -1246,6 +1378,11 @@ class Peppol extends AbstractService
// Required: TaxCategory ID (BT-118)
$category_id = new ID();
$category_id->value = 'S'; // Standard rate
if(floatval($grouped_tax['taxAmount']) === 0.0)
$category_id->value = 'K'; // Exempt
$tax_category->ID = $category_id;
// Required: TaxCategory Rate (BT-119)
@ -1342,4 +1479,72 @@ class Peppol extends AbstractService
return '0037';
}
/**
* calculateAdjustedBaseAmount
*
* Calculates the adjusted base amount for a line item considering invoice-level discounts
*
*/
private function calculateAdjustedBaseAmount(
object $line_item,
// float $invoice_discount,
// bool $is_percentage,
// array $all_line_items,
// float $allowance_charges
)
{
// 1. Calculate total invoice amount before invoice-level discount
$total_amount = 0;
foreach ($this->invoice->line_items as $item) {
$line_total = $item->quantity * $item->cost;
// Apply line-level discount if exists
if ($item->discount > 0) {
if ($item->is_amount_discount) {
$line_total -= $item->discount;
} else {
$line_total -= ($line_total * ($item->discount / 100));
}
}
$total_amount += $line_total;
}
// 2. Add any additional charges or subtract additional allowances
$total_amount += ($this->invoice->custom_surcharge1 + $this->invoice->custom_surcharge2 +$this->invoice->custom_surcharge3 + $this->invoice->custom_surcharge4);
// 3. Calculate this line item's proportion of total
$line_total = $line_item->quantity * $line_item->cost;
// Apply line-level discount if exists
if ($line_item->discount > 0) {
if ($line_item->is_amount_discount) {
$line_total -= $line_item->discount;
} else {
$line_total -= ($line_total * ($line_item->discount / 100));
}
}
$proportion = $line_total / $total_amount;
// 4. Calculate invoice-level discount amount for this line
$line_discount = 0;
if (!$this->invoice->is_amount_discount) {
$line_discount = $line_total * ($this->invoice->discount / 100);
} else {
$line_discount = $this->invoice->discount * $proportion;
}
// 5. Return adjusted base amount
return (string)round($line_total - $line_discount, 2);
}
}

View File

@ -56,6 +56,258 @@ class PeppolTest extends TestCase
);
}
public function testDeInvoiceIntraCommunitySupply()
{
$settings = CompanySettings::defaults();
$settings->address1 = 'Dudweilerstr. 34b';
$settings->city = 'Ost Alessa';
$settings->state = 'Bayern';
$settings->postal_code = '98060';
$settings->vat_number = 'DE923356489';
$settings->country_id = '276';
$settings->currency_id = '3';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
// $fib->Name = 'Deutsche Bank';
$pfa = new PayeeFinancialAccount();
$pfa->ID = 'DE89370400440532013000';
$pfa->Name = 'PFA-NAME';
// $pfa->AliasName = 'PFA-Alias';
$pfa->AccountTypeCode = 'CHECKING';
$pfa->AccountFormatCode = 'IBAN';
$pfa->CurrencyCode = 'EUR';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'e_invoice' => $stub,
]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->is_locked = false;
$cu->save();
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'German Client Name',
'address1' => 'Kinderhausen 96b',
'address2' => 'Apt. 842',
'city' => 'Süd Jessestadt',
'state' => 'Bayern',
'postal_code' => '33323',
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->discount = 0;
$item->is_amount_discount = false;
$item->tax_rate1 = 0;
$item->tax_name1 = '';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'discount' => 0,
'uses_inclusive_taxes' => false,
'status_id' => 1,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'is_amount_discount' => false,
]);
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
$this->assertEquals(100, $invoice->amount);
$peppol = new Peppol($invoice);
$peppol->setInvoiceDefaults();
$peppol->run();
nlog($peppol->toXml());
// nlog($peppol->toObject());
$de_invoice = $peppol->getInvoice();
$this->assertNotNull($de_invoice);
$e = new EInvoice();
$xml = $e->encode($de_invoice, 'xml');
$this->assertNotNull($xml);
$errors = $e->validate($de_invoice);
if(count($errors) > 0) {
nlog($errors);
}
$this->assertCount(0, $errors);
}
public function testDeInvoiceSingleInvoiceSurcharge()
{
$settings = CompanySettings::defaults();
$settings->address1 = 'Dudweilerstr. 34b';
$settings->city = 'Ost Alessa';
$settings->state = 'Bayern';
$settings->postal_code = '98060';
$settings->vat_number = 'DE923356489';
$settings->country_id = '276';
$settings->currency_id = '3';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
// $fib->Name = 'Deutsche Bank';
$pfa = new PayeeFinancialAccount();
$pfa->ID = 'DE89370400440532013000';
$pfa->Name = 'PFA-NAME';
// $pfa->AliasName = 'PFA-Alias';
$pfa->AccountTypeCode = 'CHECKING';
$pfa->AccountFormatCode = 'IBAN';
$pfa->CurrencyCode = 'EUR';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'e_invoice' => $stub,
]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->is_locked = false;
$cu->save();
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'German Client Name',
'address1' => 'Kinderhausen 96b',
'address2' => 'Apt. 842',
'city' => 'Süd Jessestadt',
'state' => 'Bayern',
'postal_code' => '33323',
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->discount = 0;
$item->is_amount_discount = false;
$item->tax_rate1 = 19;
$item->tax_name1 = 'mwst';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'discount' => 0,
'uses_inclusive_taxes' => false,
'status_id' => 1,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'is_amount_discount' => false,
]);
$invoice->custom_surcharge1 = 10;
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
$this->assertEquals(130.90, $invoice->amount);
$peppol = new Peppol($invoice);
$peppol->setInvoiceDefaults();
$peppol->run();
// $peppol->toJson()->toXml();
// nlog($peppol->toObject());
$de_invoice = $peppol->getInvoice();
$this->assertNotNull($de_invoice);
$e = new EInvoice();
$xml = $e->encode($de_invoice, 'xml');
$this->assertNotNull($xml);
$errors = $e->validate($de_invoice);
if(count($errors) > 0) {
nlog($errors);
}
$this->assertCount(0, $errors);
}
public function testDeInvoicePercentDiscounts()
{
@ -159,6 +411,7 @@ class PeppolTest extends TestCase
$peppol->setInvoiceDefaults();
$peppol->run();
// $peppol->toJson()->toXml();
// nlog($peppol->toObject());
@ -182,6 +435,455 @@ class PeppolTest extends TestCase
}
public function testDeInvoiceLevelAndItemLevelPercentageDiscount()
{
$settings = CompanySettings::defaults();
$settings->address1 = 'Dudweilerstr. 34b';
$settings->city = 'Ost Alessa';
$settings->state = 'Bayern';
$settings->postal_code = '98060';
$settings->vat_number = 'DE923356489';
$settings->id_number = '991-00110-12';
$settings->country_id = '276';
$settings->currency_id = '3';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
// $fib->Name = 'Deutsche Bank';
$pfa = new PayeeFinancialAccount();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = 'DE89370400440532013000';
$pfa->ID = $id;
$pfa->Name = 'PFA-NAME';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$pmc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\PaymentMeansCode();
$pmc->value = '30';
$pm->PaymentMeansCode = $pmc;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'e_invoice' => $stub,
]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->is_locked = false;
$cu->save();
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'German Client Name',
'address1' => 'Kinderhausen 96b',
'address2' => 'Apt. 842',
'city' => 'Süd Jessestadt',
'state' => 'Bayern',
'postal_code' => '33323',
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 100;
$item->quantity = 1;
$item->discount = 10;
$item->is_amount_discount = false;
$item->tax_rate1 = 19;
$item->tax_name1 = 'mwst';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'discount' => 10,
'uses_inclusive_taxes' => false,
'status_id' => 1,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'due_date' => now()->addDays(30)->format('Y-m-d'),
'is_amount_discount' => false,
]);
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
$this->assertEquals(96.39, $invoice->amount);
$peppol = new Peppol($invoice);
$peppol->setInvoiceDefaults();
$peppol->run();
// nlog($peppol->toXml());
$de_invoice = $peppol->getInvoice();
$this->assertNotNull($de_invoice);
$e = new EInvoice();
$xml = $e->encode($de_invoice, 'xml');
$this->assertNotNull($xml);
$errors = $e->validate($de_invoice);
if(count($errors) > 0) {
nlog($xml);
nlog($errors);
}
$this->assertCount(0, $errors);
$xml = $peppol->toXml();
try{
$processor = new \Saxon\SaxonProcessor();
}
catch(\Throwable $e){
$this->markTestSkipped('saxon not installed');
}
$validator = new XsltDocumentValidator($xml);
$validator->validate();
if(count($validator->getErrors()) >0){
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testDeInvoiceLevelPercentageDiscount()
{
$settings = CompanySettings::defaults();
$settings->address1 = 'Dudweilerstr. 34b';
$settings->city = 'Ost Alessa';
$settings->state = 'Bayern';
$settings->postal_code = '98060';
$settings->vat_number = 'DE923356489';
$settings->id_number = '991-00110-12';
$settings->country_id = '276';
$settings->currency_id = '3';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
// $fib->Name = 'Deutsche Bank';
$pfa = new PayeeFinancialAccount();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = 'DE89370400440532013000';
$pfa->ID = $id;
$pfa->Name = 'PFA-NAME';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$pmc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\PaymentMeansCode();
$pmc->value = '30';
$pm->PaymentMeansCode = $pmc;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'e_invoice' => $stub,
]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->is_locked = false;
$cu->save();
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'German Client Name',
'address1' => 'Kinderhausen 96b',
'address2' => 'Apt. 842',
'city' => 'Süd Jessestadt',
'state' => 'Bayern',
'postal_code' => '33323',
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 100;
$item->quantity = 1;
$item->discount = 0;
$item->is_amount_discount = false;
$item->tax_rate1 = 19;
$item->tax_name1 = 'mwst';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'discount' => 10,
'uses_inclusive_taxes' => false,
'status_id' => 1,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'due_date' => now()->addDays(30)->format('Y-m-d'),
'is_amount_discount' => false,
]);
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
$this->assertEquals(107.10, $invoice->amount);
$peppol = new Peppol($invoice);
$peppol->setInvoiceDefaults();
$peppol->run();
// nlog($peppol->toXml());
$de_invoice = $peppol->getInvoice();
$this->assertNotNull($de_invoice);
$e = new EInvoice();
$xml = $e->encode($de_invoice, 'xml');
$this->assertNotNull($xml);
$errors = $e->validate($de_invoice);
if(count($errors) > 0) {
nlog($xml);
nlog($errors);
}
$this->assertCount(0, $errors);
$xml = $peppol->toXml();
try{
$processor = new \Saxon\SaxonProcessor();
}
catch(\Throwable $e){
$this->markTestSkipped('saxon not installed');
}
$validator = new XsltDocumentValidator($xml);
$validator->validate();
if(count($validator->getErrors()) >0){
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testDeInvoiceAmountAndItemAmountDiscounts()
{
$settings = CompanySettings::defaults();
$settings->address1 = 'Dudweilerstr. 34b';
$settings->city = 'Ost Alessa';
$settings->state = 'Bayern';
$settings->postal_code = '98060';
$settings->vat_number = 'DE923356489';
$settings->id_number = '991-00110-12';
$settings->country_id = '276';
$settings->currency_id = '3';
$einvoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$fib = new FinancialInstitutionBranch();
$fib->ID = "DEUTDEMMXXX"; //BIC
// $fib->Name = 'Deutsche Bank';
$pfa = new PayeeFinancialAccount();
$id = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\ID();
$id->value = 'DE89370400440532013000';
$pfa->ID = $id;
$pfa->Name = 'PFA-NAME';
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
$pm->PayeeFinancialAccount = $pfa;
$pmc = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\PaymentMeansCode();
$pmc->value = '30';
$pm->PaymentMeansCode = $pmc;
$einvoice->PaymentMeans[] = $pm;
$stub = new \stdClass();
$stub->Invoice = $einvoice;
$company = Company::factory()->create([
'account_id' => $this->account->id,
'settings' => $settings,
'e_invoice' => $stub,
]);
$cu = CompanyUserFactory::create($this->user->id, $company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->is_locked = false;
$cu->save();
$client_settings = ClientSettings::defaults();
$client_settings->currency_id = '3';
$client = Client::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'name' => 'German Client Name',
'address1' => 'Kinderhausen 96b',
'address2' => 'Apt. 842',
'city' => 'Süd Jessestadt',
'state' => 'Bayern',
'postal_code' => '33323',
'country_id' => 276,
'routing_id' => 'ABC1234',
'settings' => $client_settings,
]);
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->discount = 5;
$item->is_amount_discount = true;
$item->tax_rate1 = 19;
$item->tax_name1 = 'mwst';
$invoice = Invoice::factory()->create([
'company_id' => $company->id,
'user_id' => $this->user->id,
'client_id' => $client->id,
'discount' => 5,
'uses_inclusive_taxes' => false,
'status_id' => 1,
'tax_rate1' => 0,
'tax_name1' => '',
'tax_rate2' => 0,
'tax_rate3' => 0,
'tax_name2' => '',
'tax_name3' => '',
'line_items' => [$item],
'number' => 'DE-'.rand(1000, 100000),
'date' => now()->format('Y-m-d'),
'due_date' => now()->addDays(30)->format('Y-m-d'),
'is_amount_discount' => true,
]);
$invoice = $invoice->calc()->getInvoice();
$invoice->service()->markSent()->save();
$this->assertEquals(107.1, $invoice->amount);
$peppol = new Peppol($invoice);
$peppol->setInvoiceDefaults();
$peppol->run();
$de_invoice = $peppol->getInvoice();
$this->assertNotNull($de_invoice);
$e = new EInvoice();
$xml = $e->encode($de_invoice, 'xml');
$this->assertNotNull($xml);
$errors = $e->validate($de_invoice);
if(count($errors) > 0) {
nlog($errors);
}
$this->assertCount(0, $errors);
$xml = $peppol->toXml();
try{
$processor = new \Saxon\SaxonProcessor();
}
catch(\Throwable $e){
$this->markTestSkipped('saxon not installed');
}
$validator = new XsltDocumentValidator($xml);
$validator->validate();
if(count($validator->getErrors()) > 0)
{
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());
}
public function testDeInvoiceAmountDiscounts()
{
@ -207,18 +909,6 @@ class PeppolTest extends TestCase
$pfa->ID = $id;
$pfa->Name = 'PFA-NAME';
// $code = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\AccountTypeCode();
// $code->value = 'CHECKING';
// $pfa->AccountTypeCode = $code;
// $code = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\AccountFormatCode();
// $code->value = 'IBAN';
// $pfa->AccountFormatCode = $code;
// $code = new \InvoiceNinja\EInvoice\Models\Peppol\CodeType\CurrencyCode();
// $code->value = 'EUR';
// $pfa->CurrencyCode = $code;
$pfa->FinancialInstitutionBranch = $fib;
$pm = new PaymentMeans();
@ -320,6 +1010,8 @@ class PeppolTest extends TestCase
$xml = $peppol->toXml();
try{
$processor = new \Saxon\SaxonProcessor();
}
@ -330,7 +1022,11 @@ class PeppolTest extends TestCase
$validator = new XsltDocumentValidator($xml);
$validator->validate();
nlog($validator->getErrors());
if(count($validator->getErrors()) > 0)
{
nlog($xml);
nlog($validator->getErrors());
}
$this->assertCount(0, $validator->getErrors());