This commit is contained in:
David Bomba 2024-10-29 12:11:52 +11:00
parent 5790e71e73
commit a74dba5a5d
9 changed files with 318 additions and 83 deletions

View File

@ -362,7 +362,7 @@ class Company extends BaseModel
'smtp_local_domain',
'smtp_verify_peer',
// 'e_invoice',
'e_invoicing_token',
// 'e_invoicing_token',
];
protected $hidden = [

View File

@ -13,17 +13,8 @@ namespace App\Services\EDocument\Gateway\Storecove\Models;
class AllowanceCharges
{
public string $reason;
public float $amountExcludingTax;
public Tax $tax;
public function __construct(
string $reason,
float $amountExcludingTax,
Tax $tax
) {
$this->reason = $reason;
$this->amountExcludingTax = $amountExcludingTax;
$this->tax = $tax;
}
public string $reason,
public float $amountExcludingTax
) {}
}

View File

@ -21,7 +21,7 @@ class InvoiceLines
public string $description = '';
public Tax $taxesDutiesFees;
public Tax $tax;
public array $allowanceCharges = []; //line item discounts
@ -102,14 +102,14 @@ class InvoiceLines
public function getTax(): Tax
{
return $this->taxesDutiesFees;
return $this->tax;
}
public function setTax(Tax $tax): void
{
$this->taxesDutiesFees = $tax;
$this->tax = $tax;
}
public function getAllowanceCharges(): array
{
return $this->allowanceCharges;
@ -125,6 +125,7 @@ class InvoiceLines
public function addAllowanceCharge($allowanceCharge): self
{
$this->allowanceCharges[] = $allowanceCharge;
return $this;
}
}

View File

@ -163,8 +163,8 @@ class PaymentMeans
if($ubl_payment_means->PayeeFinancialAccount ?? false){
$this->account = $ubl_payment_means->PayeeFinancialAccount->ID;
$this->branche_code = $ubl_payment_means->FinancialInstitutionBranch->ID;
$this->account = $ubl_payment_means->PayeeFinancialAccount->ID->value;
$this->branche_code = $ubl_payment_means->FinancialInstitutionBranch->ID->value ?? null;
}

View File

@ -2,15 +2,17 @@
namespace App\Services\EDocument\Gateway\Transformers;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingCustomerParty;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingSupplierParty;
use App\Services\EDocument\Gateway\Storecove\Models\Tax;
use App\Services\EDocument\Gateway\Storecove\Models\Party;
use App\Services\EDocument\Gateway\Storecove\Models\Address;
use App\Services\EDocument\Gateway\Storecove\Models\Contact;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
use App\Services\EDocument\Gateway\Storecove\Models\InvoiceLines;
use App\Services\EDocument\Gateway\Storecove\Models\Party;
use App\Services\EDocument\Gateway\Storecove\Models\PaymentMeans;
use App\Services\EDocument\Gateway\Storecove\Models\References;
use App\Services\EDocument\Gateway\Storecove\Models\InvoiceLines;
use App\Services\EDocument\Gateway\Storecove\Models\PaymentMeans;
use App\Services\EDocument\Gateway\Storecove\Models\AllowanceCharges;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingCustomerParty;
use App\Services\EDocument\Gateway\Storecove\Models\AccountingSupplierParty;
use App\Services\EDocument\Gateway\Storecove\Models\Invoice as StorecoveInvoice;
class StorecoveTransformer implements TransformerInterface
{
@ -32,7 +34,7 @@ class StorecoveTransformer implements TransformerInterface
if (isset($peppolInvoice->InvoicePeriod[0]) &&
isset($peppolInvoice->InvoicePeriod[0]->StartDate) &&
isset($peppolInvoice->InvoicePeriod[0]->EndDate)) {
$this->s_invoice->setInvoicePeriod("{$peppolInvoice->InvoicePeriod[0]->StartDate} - {$peppolInvoice->InvoicePeriod[0]->EndDate}");
$this->s_invoice->setInvoicePeriod("{$peppolInvoice->InvoicePeriod[0]->StartDate->format('Y-m-d')} - {$peppolInvoice->InvoicePeriod[0]->EndDate->format('Y-m-d')}");
}
if($peppolInvoice->BuyerReference ?? false){
@ -41,7 +43,7 @@ class StorecoveTransformer implements TransformerInterface
}
if ($peppolInvoice->OrderReference->ID ?? false) {
$ref = new References(documentId: $peppolInvoice->OrderReference->ID, documentType: 'sales_order');
$ref = new References(documentId: $peppolInvoice->OrderReference->ID->value, documentType: 'sales_order');
$this->s_invoice->addReferences($ref);
}
@ -57,7 +59,7 @@ class StorecoveTransformer implements TransformerInterface
city: $peppolInvoice->AccountingCustomerParty->Party->PostalAddress->CityName,
zip: $peppolInvoice->AccountingCustomerParty->Party->PostalAddress->PostalZone,
county: $peppolInvoice->AccountingCustomerParty->Party->PostalAddress->CountrySubentity ?? null,
country: $peppolInvoice->AccountingCustomerParty->Party->PostalAddress->Country->IdentificationCode,
country: $peppolInvoice->AccountingCustomerParty->Party->PostalAddress->Country->IdentificationCode->value,
);
$contact = new Contact(
@ -90,11 +92,52 @@ class StorecoveTransformer implements TransformerInterface
$this->s_invoice->addPaymentMeans($payment_means);
}
foreach($peppolInvoice->InvoiceLine as $invoiceLine)
$lines = [];
foreach($peppolInvoice->InvoiceLine as $peppolLine)
{
$line = new InvoiceLines();
// Basic line details
$line->setLineId($peppolLine->ID->value);
$line->setQuantity((int)$peppolLine->InvoicedQuantity);
$line->setItemPrice((float)$peppolLine->Price->PriceAmount->amount);
$line->setAmountExcludingVat((float)$peppolLine->LineExtensionAmount->amount);
// Item details
$line->setName($peppolLine->Item->Name);
$line->setDescription($peppolLine->Item->Description);
// Tax handling
if(isset($peppolLine->Item->ClassifiedTaxCategory) && is_array($peppolLine->Item->ClassifiedTaxCategory)){
foreach($peppolLine->Item->ClassifiedTaxCategory as $ctc)
{
$tax = new Tax((float)$ctc->Percent, $this->resolveJurisdication($ctc, $peppolInvoice));
$line->setTax($tax);
}
}
//discounts
if(isset($peppolLine->Price->AllowanceCharge) && is_array($peppolLine->Price->AllowanceCharge)){
foreach($peppolLine->Price->AllowanceCharge as $allowance)
{
$reason = $allowance->ChargeIndicator ? ctrans('texts.fee') : ctrans('texts.discount');
$amount = $allowance->Amount->amount;
$ac = new AllowanceCharges(reason: $reason, amountExcludingTax: $amount);
$line->addAllowanceCharge($ac);
}
}
$lines[] = $line;
}
$this->s_invoice->invoiceLines = $lines;
// // Map tax total at invoice level
// $taxTotal = [];
// if (isset($peppolInvoice->InvoiceLine[0]->TaxTotal[0])) {
@ -143,6 +186,14 @@ class StorecoveTransformer implements TransformerInterface
}
private function resolveJurisdication($ctc, $peppolInvoice): string
{
if(isset($ctc->TaxTotal[0]->JurisdictionRegionAddress->Country->IdentificationCode->value))
return $ctc->TaxTotal[0]->JurisdictionRegionAddress->Country->IdentificationCode->value;
return $peppolInvoice->AccountingSupplierParty->Party->PhysicalLocation->Country->IdentificationCode->value;
}
public function getInvoice(): StorecoveInvoice
{
return $this->s_invoice;

View File

@ -11,6 +11,7 @@
namespace App\Services\EDocument\Standards;
use App\DataMapper\Tax\BaseRule;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Product;
@ -671,13 +672,59 @@ class Peppol extends AbstractService
$line->TaxTotal = $item_taxes;
}
$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;
// $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;
// 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)$this->getBasePrice($item);
$basePrice->PriceAmount = $basePriceAmount;
// Add Allowance Charge to Price
$allowanceCharge = new \InvoiceNinja\EInvoice\Models\Peppol\AllowanceChargeType\AllowanceCharge();
$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)$this->getBasePrice($item);
// Add percentage if available
if ($item->discount > 0 && !$item->is_amount_discount) {
$mfn = new \InvoiceNinja\EInvoice\Models\Peppol\NumericType\MultiplierFactorNumeric();
$mfn->value = (string) ($item->discount / 100);
$allowanceCharge->MultiplierFactorNumeric = $mfn; // Convert percentage to decimal
}
$basePrice->AllowanceCharge[] = $allowanceCharge;
$line->Price = $basePrice;
} 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));
$price->PriceAmount = $pa;
$line->Price = $price;
}
$line->Price = $price;
$lines[] = $line;
}
@ -685,6 +732,21 @@ class Peppol extends AbstractService
return $lines;
}
private function getBasePrice($item): float
{
return $item->cost;
}
private function calculateDiscountAmount($item): float
{
if ($item->is_amount_discount) {
return $item->discount; // Per unit discount amount
}
return $item->cost * ($item->discount / 100);
}
/**
* costWithDiscount
@ -772,7 +834,6 @@ class Peppol extends AbstractService
$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();
@ -785,6 +846,9 @@ class Peppol extends AbstractService
$id = new ID();
$id->value = $item->tax_name1;
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
@ -824,6 +888,9 @@ class Peppol extends AbstractService
$id = new ID();
$id->value = $item->tax_name2;
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
@ -866,6 +933,9 @@ class Peppol extends AbstractService
$id = new ID();
$id->value = $item->tax_name3;
$jurisdiction = $this->getJurisdiction();
$ts->JurisdictionRegionAddress[] = $jurisdiction;
$ts->ID = $id;
$tc->TaxScheme = $ts;
$tax_subtotal->TaxCategory = $tc;
@ -908,7 +978,7 @@ class Peppol extends AbstractService
$vatID->schemeID = $scheme;
}
$vatID->value = $this->company->settings->vat_number; //todo if we are cross border - switch to the supplier local vat number
$vatID->value = $this->company->settings->vat_number; //todo if we are cross border - switch to the supplier local vat number ->vat_number; //todo if we are cross border - switch to the supplier local vat number
$pi->ID = $vatID;
$party->PartyIdentification[] = $pi;
@ -921,7 +991,6 @@ class Peppol extends AbstractService
// $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();
@ -1018,7 +1087,7 @@ class Peppol extends AbstractService
$contact->ElectronicMail = $this->invoice->client->present()->email();
if(isset($this->invoice->client->phone) && strlen($this->invoice->client->phone >2))
if(isset($this->invoice->client->phone) && strlen($this->invoice->client->phone) > 2)
$contact->Telephone = $this->invoice->client->phone;
$party->Contact = $contact;
@ -1091,49 +1160,82 @@ class Peppol extends AbstractService
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;
}
// 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->gateway->mutator->getSetting($prop)) {
$this->p_invoice->{$prop} = $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->gateway->mutator->getSetting($prop)) {
$this->p_invoice->{$prop} = $prop_value;
}
}
return $this;
return $this;
}
public function getJurisdiction()
{
//calculate nexus
$country_code = $this->company->country()->iso_3166_2;
$br = new BaseRule();
$eu_countries = $br->eu_country_codes;
if($this->invoice->client->country->iso_3166_2 == $this->company->country()->iso_3166_2){
//Domestic Sales
$country_code = $this->company->country()->iso_3166_2;
}
elseif(in_array($country_code, $eu_countries) && !in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){
//NON-EU sale
}
elseif(in_array($country_code, $eu_countries) && in_array($this->invoice->client->country->iso_3166_2, $eu_countries)){
//EU Sale
if($this->company->tax_data->regions->EU->has_sales_above_threshold || !$this->invoice->client->has_valid_vat_number){ //over threshold - tax in buyer country
$country_code = $this->invoice->client->country->iso_3166_2;
}
}
$jurisdiction = new \InvoiceNinja\EInvoice\Models\Peppol\AddressType\JurisdictionRegionAddress();
$country = new Country();
$ic = new IdentificationCode();
$ic->value = $country_code;
$country->IdentificationCode = $ic;
$jurisdiction->Country = $country;
return $jurisdiction;
}
}

View File

@ -178,7 +178,7 @@ class ZugferdEDokument extends AbstractService
if (!$company->tax_data->regions->EU->has_sales_above_threshold){
// According to european law, each line item can only have one tax rate
if (!(empty($item->tax_name1) && empty($item->tax_name2) && empty($item->tax_name3))) {
$taxtype = $this->getTaxType($item->tax_id);
$taxtype = $this->getTaxType($item->tax_id ?? 2);
if (!empty($item->tax_name1)) {
if ($taxtype == ZugferdDutyTaxFeeCategories::VAT_EXEMPT_FOR_EEA_INTRACOMMUNITY_SUPPLY_OF_GOODS_AND_SERVICES) {
$this->xdocument->addDocumentPositionTax($taxtype, 'VAT', $item->tax_rate1, exemptionReason: ctrans('texts.intracommunity_tax_info'));

View File

@ -8,8 +8,8 @@ return new class extends Migration
{
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->string('e_invoicing_token')->nullable();
});
// Schema::table('companies', function (Blueprint $table) {
// $table->string('e_invoicing_token')->nullable();
// });
}
};

View File

@ -95,6 +95,96 @@ class StorecoveTest extends TestCase
nlog($json);
}
public function testStorecoveTransformer()
{
$e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$invoice = $this->createATData();
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->is_amount_discount = true;
$item->discount=5;
$item->tax_rate1 = 20;
$item->tax_name1 = 'VAT';
$invoice->line_items = [$item];
$invoice->calc()->getInvoice();
$stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}');
foreach ($stub as $key => $value) {
$e_invoice->{$key} = $value;
}
$invoice->e_invoice = $e_invoice;
$p = new Peppol($invoice);
$p->run();
$peppolInvoice = $p->getInvoice();
$s_transformer = new StorecoveTransformer();
$s_transformer->transform($peppolInvoice);
$json = $s_transformer->toJson();
$this->assertJson($json);
nlog($json);
}
public function testStorecoveTransformerWithPercentageDiscount()
{
$e_invoice = new \InvoiceNinja\EInvoice\Models\Peppol\Invoice();
$invoice = $this->createATData();
$invoice->is_amount_discount = false;
$item = new InvoiceItem();
$item->product_key = "Product Key";
$item->notes = "Product Description";
$item->cost = 10;
$item->quantity = 10;
$item->is_amount_discount = false;
$item->discount=5;
$item->tax_rate1 = 20;
$item->tax_name1 = 'VAT';
$invoice->line_items = [$item];
$invoice->calc()->getInvoice();
$stub = json_decode('{"Invoice":{"Note":"Nooo","PaymentMeans":[{"ID":{"value":"afdasfasdfasdfas"},"PayeeFinancialAccount":{"Name":"PFA-NAME","ID":{"value":"DE89370400440532013000"},"AliasName":"PFA-Alias","AccountTypeCode":{"value":"CHECKING"},"AccountFormatCode":{"value":"IBAN"},"CurrencyCode":{"value":"EUR"},"FinancialInstitutionBranch":{"ID":{"value":"DEUTDEMMXXX"},"Name":"Deutsche Bank"}}}]}}');
foreach ($stub as $key => $value) {
$e_invoice->{$key} = $value;
}
$invoice->e_invoice = $e_invoice;
$p = new Peppol($invoice);
$p->run();
$peppolInvoice = $p->getInvoice();
$s_transformer = new StorecoveTransformer();
$s_transformer->transform($peppolInvoice);
$json = $s_transformer->toJson();
$this->assertJson($json);
nlog("percentage");
nlog($json);
}
public function testUnsetOfVatNumers()
{