mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
commit
94838cbdc4
27
app/Casts/EncryptedCast.php
Normal file
27
app/Casts/EncryptedCast.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
|
||||
class EncryptedCast implements CastsAttributes
|
||||
{
|
||||
public function get($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return strlen($value) > 1 ? decrypt($value) : null;
|
||||
}
|
||||
|
||||
public function set($model, string $key, $value, array $attributes)
|
||||
{
|
||||
return [$key => ! is_null($value) ? encrypt($value) : null];
|
||||
}
|
||||
}
|
@ -14,9 +14,9 @@ namespace App\DataMapper\Tax;
|
||||
use App\Models\Client;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\DataMapper\Tax\TaxData;
|
||||
use App\DataProviders\USStates;
|
||||
use App\DataMapper\Tax\ZipTax\Response;
|
||||
use App\Services\Tax\Providers\TaxProvider;
|
||||
|
||||
class BaseRule implements RuleInterface
|
||||
{
|
||||
@ -104,9 +104,6 @@ class BaseRule implements RuleInterface
|
||||
/** EU TAXES */
|
||||
|
||||
|
||||
/** US TAXES */
|
||||
/** US TAXES */
|
||||
|
||||
public string $tax_name1 = '';
|
||||
public float $tax_rate1 = 0;
|
||||
|
||||
@ -131,69 +128,139 @@ class BaseRule implements RuleInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the tax rule for the entity.
|
||||
*
|
||||
* @param mixed $invoice
|
||||
* @return self
|
||||
*/
|
||||
public function setEntity(mixed $invoice): self
|
||||
{
|
||||
$this->invoice = $invoice;
|
||||
|
||||
$this->client = $invoice->client;
|
||||
|
||||
$this->configTaxData()
|
||||
->resolveRegions();
|
||||
$this->resolveRegions();
|
||||
|
||||
if(!$this->isTaxableRegion())
|
||||
return $this;
|
||||
|
||||
$this->configTaxData();
|
||||
|
||||
$this->tax_data = new Response($this->invoice->tax_data);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configigures the Tax Data for the entity
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function configTaxData(): self
|
||||
{
|
||||
|
||||
/* We should only apply taxes for configured states */
|
||||
if(!array_key_exists($this->client->country->iso_3166_2, $this->region_codes)) {
|
||||
$this->client->country_id = $this->invoice->company->settings->country_id;
|
||||
$this->client->saveQuietly();
|
||||
nlog('Automatic tax calculations not supported for this country - defaulting to company country');
|
||||
nlog("With new logic, we should never see this");
|
||||
}
|
||||
|
||||
$this->client_region = $this->region_codes[$this->client->country->iso_3166_2];
|
||||
/** Harvest the client_region */
|
||||
|
||||
/** If the tax data is already set and the invoice is marked as sent, do not adjust the rates */
|
||||
if($this->invoice->tax_data && $this->invoice->status_id > 1)
|
||||
return $this;
|
||||
|
||||
//determine if we are taxing locally or if we are taxing globally
|
||||
$tax_data = is_object($this->invoice->client->tax_data) ? $this->invoice->client->tax_data : new Response([]);
|
||||
/**
|
||||
* Origin - Company Tax Data
|
||||
* Destination - Client Tax Data
|
||||
*
|
||||
*/
|
||||
// $tax_data = new Response([]);
|
||||
$tax_data = false;
|
||||
|
||||
if(strlen($this->invoice->tax_data?->originDestination) == 0 && $this->client->company->tax_data->seller_subregion != $this->client_subregion) {
|
||||
$tax_data->originDestination = "D";
|
||||
$tax_data->geoState = $this->client_subregion;
|
||||
if($this->seller_region == 'US' && $this->client_region == 'US'){
|
||||
|
||||
$company = $this->invoice->company;
|
||||
|
||||
/** If no company tax data has been configured, lets do that now. */
|
||||
if(!$company->origin_tax_data && \DB::transactionLevel() == 0)
|
||||
{
|
||||
|
||||
$tp = new TaxProvider($company);
|
||||
$tp->updateCompanyTaxData();
|
||||
$company->fresh();
|
||||
|
||||
}
|
||||
|
||||
/** If we are in a Origin based state, force the company tax here */
|
||||
if($company->origin_tax_data->originDestination == 'O' && ($company->tax_data->seller_subregion == $this->client_subregion)) {
|
||||
|
||||
$tax_data = $company->origin_tax_data;
|
||||
|
||||
}
|
||||
else{
|
||||
|
||||
/** Ensures the client tax data has been updated */
|
||||
if(!$this->client->tax_data && \DB::transactionLevel() == 0) {
|
||||
|
||||
$tp = new TaxProvider($company, $this->client);
|
||||
$tp->updateClientTaxData();
|
||||
$this->client->fresh();
|
||||
}
|
||||
|
||||
$tax_data = $this->client->tax_data;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Applies the tax data to the invoice */
|
||||
if($this->invoice instanceof Invoice && $tax_data) {
|
||||
|
||||
if($this->invoice instanceof Invoice) {
|
||||
$this->invoice->tax_data = $tax_data ;
|
||||
|
||||
if(\DB::transactionLevel() == 0)
|
||||
$this->invoice->saveQuietly();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
// Refactor to support switching between shipping / billing country / region / subregion
|
||||
|
||||
/**
|
||||
* Resolve Regions & Subregions
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
private function resolveRegions(): self
|
||||
{
|
||||
|
||||
$this->client_region = $this->region_codes[$this->client->country->iso_3166_2];
|
||||
|
||||
match($this->client_region){
|
||||
'US' => $this->client_subregion = strlen($this->invoice?->tax_data?->geoState) > 1 ? $this->invoice?->tax_data?->geoState : $this->getUSState(),
|
||||
'US' => $this->client_subregion = strlen($this->invoice?->client?->tax_data?->geoState) > 1 ? $this->invoice->client->tax_data->geoState : $this->getUSState(),
|
||||
'EU' => $this->client_subregion = $this->client->country->iso_3166_2,
|
||||
'AU' => $this->client_subregion = 'AU',
|
||||
default => $this->client_subregion = $this->client->country->iso_3166_2,
|
||||
};
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
private function getUSState(): string
|
||||
{
|
||||
try {
|
||||
|
||||
$states = USStates::$states;
|
||||
|
||||
if(isset($states[$this->client->state]))
|
||||
return $this->client->state;
|
||||
|
||||
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->client->company->country()->iso_3166_2 == 'US' ? $this->client->company->tax_data->seller_subregion : 'CA';
|
||||
}
|
||||
@ -207,7 +274,7 @@ class BaseRule implements RuleInterface
|
||||
public function defaultForeign(): self
|
||||
{
|
||||
|
||||
if($this->client_region == 'US') {
|
||||
if($this->client_region == 'US' && isset($this->tax_data?->taxSales)) {
|
||||
|
||||
$this->tax_rate1 = $this->tax_data->taxSales * 100;
|
||||
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
|
||||
@ -235,18 +302,21 @@ class BaseRule implements RuleInterface
|
||||
{
|
||||
|
||||
if ($this->client->is_tax_exempt) {
|
||||
return $this->taxExempt();
|
||||
|
||||
return $this->taxExempt($item);
|
||||
|
||||
} elseif($this->client_region == $this->seller_region && $this->isTaxableRegion()) {
|
||||
|
||||
$this->taxByType($item->tax_id);
|
||||
$this->taxByType($item);
|
||||
|
||||
return $this;
|
||||
|
||||
} elseif($this->isTaxableRegion()) { //other regions outside of US
|
||||
|
||||
match(intval($item->tax_id)) {
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(),
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
|
||||
default => $this->defaultForeign(),
|
||||
};
|
||||
|
||||
@ -260,42 +330,42 @@ class BaseRule implements RuleInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxReduced(): self
|
||||
public function taxReduced($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxExempt(): self
|
||||
public function taxExempt($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxDigital(): self
|
||||
public function taxDigital($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxService(): self
|
||||
public function taxService($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxShipping(): self
|
||||
public function taxShipping($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function taxPhysical(): self
|
||||
public function taxPhysical($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function default(): self
|
||||
public function default($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function override(): self
|
||||
public function override($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
@ -304,4 +374,10 @@ class BaseRule implements RuleInterface
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function regionWithNoTaxCoverage(string $iso_3166_2): bool
|
||||
{
|
||||
return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes)));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,10 +30,10 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public bool $eu_business_tax_exempt = true;
|
||||
|
||||
/** @var bool $foreign_business_tax_exempt */
|
||||
public bool $foreign_business_tax_exempt = true;
|
||||
public bool $foreign_business_tax_exempt = false;
|
||||
|
||||
/** @var bool $foreign_consumer_tax_exempt */
|
||||
public bool $foreign_consumer_tax_exempt = true;
|
||||
public bool $foreign_consumer_tax_exempt = false;
|
||||
|
||||
/** @var float $tax_rate */
|
||||
public float $tax_rate = 0;
|
||||
@ -56,25 +56,27 @@ class Rule extends BaseRule implements RuleInterface
|
||||
/**
|
||||
* Sets the correct tax rate based on the product type.
|
||||
*
|
||||
* @param mixed $product_tax_type
|
||||
* @param mixed $item
|
||||
* @return self
|
||||
*/
|
||||
public function taxByType($product_tax_type): self
|
||||
public function taxByType($item): self
|
||||
{
|
||||
|
||||
if ($this->client->is_tax_exempt) {
|
||||
return $this->taxExempt();
|
||||
return $this->taxExempt($item);
|
||||
}
|
||||
|
||||
match(intval($product_tax_type)){
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(),
|
||||
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(),
|
||||
Product::PRODUCT_TYPE_SERVICE => $this->taxService(),
|
||||
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(),
|
||||
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(),
|
||||
default => $this->default(),
|
||||
match(intval($item->tax_id)){
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
|
||||
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item),
|
||||
Product::PRODUCT_TYPE_SERVICE => $this->taxService($item),
|
||||
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item),
|
||||
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
|
||||
Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item),
|
||||
Product::PRODUCT_TYPE_REVERSE_TAX => $this->reverseTax($item),
|
||||
default => $this->default($item),
|
||||
};
|
||||
|
||||
return $this;
|
||||
@ -85,7 +87,20 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxReduced(): self
|
||||
public function reverseTax($item): self
|
||||
{
|
||||
$this->tax_rate1 = 0;
|
||||
$this->tax_name1 = 'ermäßigte MwSt.';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a reduced tax product
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxReduced($item): self
|
||||
{
|
||||
$this->tax_rate1 = $this->reduced_tax_rate;
|
||||
$this->tax_name1 = 'ermäßigte MwSt.';
|
||||
@ -93,12 +108,26 @@ class Rule extends BaseRule implements RuleInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a zero rated tax product
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function zeroRated($item): self
|
||||
{
|
||||
$this->tax_rate1 = 0;
|
||||
$this->tax_name1 = 'ermäßigte MwSt.';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a tax exempt product
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxExempt(): self
|
||||
public function taxExempt($item): self
|
||||
{
|
||||
$this->tax_name1 = '';
|
||||
$this->tax_rate1 = 0;
|
||||
@ -111,7 +140,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxDigital(): self
|
||||
public function taxDigital($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
@ -125,7 +154,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxService(): self
|
||||
public function taxService($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
@ -139,7 +168,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxShipping(): self
|
||||
public function taxShipping($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
@ -153,7 +182,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxPhysical(): self
|
||||
public function taxPhysical($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = $this->tax_rate;
|
||||
@ -167,7 +196,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function default(): self
|
||||
public function default($item): self
|
||||
{
|
||||
|
||||
$this->tax_name1 = '';
|
||||
@ -181,7 +210,7 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function override(): self
|
||||
public function override($item): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
@ -194,38 +223,42 @@ class Rule extends BaseRule implements RuleInterface
|
||||
public function calculateRates(): self
|
||||
{
|
||||
if ($this->client->is_tax_exempt) {
|
||||
// nlog("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->has_valid_vat_number && $this->eu_business_tax_exempt)
|
||||
{
|
||||
// nlog("euro zone and tax exempt");
|
||||
nlog("euro zone and 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");
|
||||
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) && !$this->client->has_valid_vat_number) //eu country / no valid vat
|
||||
{
|
||||
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold)
|
||||
{
|
||||
// nlog("eu zone with sales above threshold");
|
||||
nlog("eu zone with sales above threshold");
|
||||
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate;
|
||||
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate;
|
||||
}
|
||||
else {
|
||||
// nlog("EU with intra-community supply ie DE to DE");
|
||||
nlog("EU with intra-community supply ie DE to DE");
|
||||
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
|
||||
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// nlog("default tax");
|
||||
nlog("default tax");
|
||||
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->tax_rate;
|
||||
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->company->country()->iso_3166_2}->reduced_tax_rate;
|
||||
}
|
||||
|
@ -15,25 +15,25 @@ interface RuleInterface
|
||||
{
|
||||
public function init();
|
||||
|
||||
public function tax($item = null);
|
||||
public function tax($item);
|
||||
|
||||
public function taxByType($type);
|
||||
|
||||
public function taxExempt();
|
||||
public function taxExempt($item);
|
||||
|
||||
public function taxDigital();
|
||||
public function taxDigital($item);
|
||||
|
||||
public function taxService();
|
||||
public function taxService($item);
|
||||
|
||||
public function taxShipping();
|
||||
public function taxShipping($item);
|
||||
|
||||
public function taxPhysical();
|
||||
public function taxPhysical($item);
|
||||
|
||||
public function taxReduced();
|
||||
public function taxReduced($item);
|
||||
|
||||
public function default();
|
||||
public function default($item);
|
||||
|
||||
public function override();
|
||||
public function override($item);
|
||||
|
||||
public function calculateRates();
|
||||
}
|
@ -41,31 +41,39 @@ class Rule extends BaseRule implements RuleInterface
|
||||
/**
|
||||
* Override tax class, we use this when we do not modify the input taxes
|
||||
*
|
||||
* @param mixed $item
|
||||
* @return self
|
||||
*/
|
||||
public function override(): self
|
||||
public function override($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = $item->tax_rate1;
|
||||
|
||||
$this->tax_name1 = $item->tax_name1;
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the correct tax rate based on the product type.
|
||||
*
|
||||
* @param mixed $product_tax_type
|
||||
* @param mixed $item
|
||||
* @return self
|
||||
*/
|
||||
public function taxByType($product_tax_type): self
|
||||
public function taxByType($item): self
|
||||
{
|
||||
|
||||
match(intval($product_tax_type)) {
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt(),
|
||||
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital(),
|
||||
Product::PRODUCT_TYPE_SERVICE => $this->taxService(),
|
||||
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping(),
|
||||
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical(),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced(),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override(),
|
||||
default => $this->default(),
|
||||
match(intval($item->tax_id)) {
|
||||
Product::PRODUCT_TYPE_EXEMPT => $this->taxExempt($item),
|
||||
Product::PRODUCT_TYPE_DIGITAL => $this->taxDigital($item),
|
||||
Product::PRODUCT_TYPE_SERVICE => $this->taxService($item),
|
||||
Product::PRODUCT_TYPE_SHIPPING => $this->taxShipping($item),
|
||||
Product::PRODUCT_TYPE_PHYSICAL => $this->taxPhysical($item),
|
||||
Product::PRODUCT_TYPE_REDUCED_TAX => $this->taxReduced($item),
|
||||
Product::PRODUCT_TYPE_OVERRIDE_TAX => $this->override($item),
|
||||
Product::PRODUCT_TYPE_ZERO_RATED => $this->zeroRated($item),
|
||||
default => $this->default($item),
|
||||
};
|
||||
|
||||
return $this;
|
||||
@ -73,10 +81,11 @@ class Rule extends BaseRule implements RuleInterface
|
||||
|
||||
/**
|
||||
* Sets the tax as exempt (0)
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxExempt(): self
|
||||
public function taxExempt($item): self
|
||||
{
|
||||
$this->tax_name1 = '';
|
||||
$this->tax_rate1 = 0;
|
||||
@ -86,25 +95,27 @@ class Rule extends BaseRule implements RuleInterface
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a digital product
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxDigital(): self
|
||||
public function taxDigital($item): self
|
||||
{
|
||||
$this->default();
|
||||
$this->default($item);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a service product
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxService(): self
|
||||
public function taxService($item): self
|
||||
{
|
||||
if($this->tax_data?->txbService == 'Y') {
|
||||
$this->default();
|
||||
if(in_array($this->tax_data?->txbService,['Y','L'])) {
|
||||
$this->default($item);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -112,13 +123,15 @@ class Rule extends BaseRule implements RuleInterface
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a shipping product
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxShipping(): self
|
||||
public function taxShipping($item): self
|
||||
{
|
||||
|
||||
if($this->tax_data?->txbFreight == 'Y') {
|
||||
$this->default();
|
||||
$this->default($item);
|
||||
}
|
||||
|
||||
return $this;
|
||||
@ -126,12 +139,15 @@ class Rule extends BaseRule implements RuleInterface
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a physical product
|
||||
* @param mixed $item
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxPhysical(): self
|
||||
public function taxPhysical($item): self
|
||||
{
|
||||
$this->default();
|
||||
nlog("tax physical");
|
||||
nlog($item);
|
||||
$this->default($item);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -141,32 +157,31 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function default(): self
|
||||
public function default($item): self
|
||||
{
|
||||
|
||||
if($this->tax_data?->stateSalesTax == 0) {
|
||||
|
||||
if($this->tax_data->originDestination == "O"){
|
||||
$tax_region = $this->client->company->tax_data->seller_subregion;
|
||||
$this->tax_rate1 = $this->invoice->client->company->tax_data->regions->US->subregions->{$tax_region}->tax_rate;
|
||||
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
|
||||
} else {
|
||||
$this->tax_rate1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_rate;
|
||||
$this->tax_name1 = $this->invoice->client->company->tax_data->regions->{$this->client_region}->subregions->{$this->client_subregion}->tax_name;
|
||||
|
||||
if($this->client_region == 'US')
|
||||
$this->tax_name1 = "{$this->client_subregion} ".$this->tax_name1;
|
||||
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->tax_rate1 = $this->tax_data->taxSales * 100;
|
||||
$this->tax_name1 = "{$this->tax_data->geoState} Sales Tax";
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function zeroRated($item): self
|
||||
{
|
||||
|
||||
$this->tax_rate1 = 0;
|
||||
$this->tax_name1 = "{$this->tax_data->geoState} Zero Rated Tax";
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -174,9 +189,21 @@ class Rule extends BaseRule implements RuleInterface
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function taxReduced(): self
|
||||
public function taxReduced($item): self
|
||||
{
|
||||
$this->default();
|
||||
$this->default($item);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the tax rate for a reverse tax product
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function reverseTax($item): self
|
||||
{
|
||||
$this->default($item);
|
||||
|
||||
return $this;
|
||||
}
|
||||
@ -190,4 +217,5 @@ class Rule extends BaseRule implements RuleInterface
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ use Illuminate\Support\Facades\Http;
|
||||
|
||||
class USStates
|
||||
{
|
||||
protected static array $states = [
|
||||
public static array $states = [
|
||||
'AL' => 'Alabama',
|
||||
'AK' => 'Alaska',
|
||||
'AZ' => 'Arizona',
|
||||
|
@ -29,6 +29,36 @@ class InvoiceItemSum
|
||||
use Discounter;
|
||||
use Taxer;
|
||||
|
||||
private array $eu_tax_jurisdictions = [
|
||||
'AT', // Austria
|
||||
'BE', // Belgium
|
||||
'BG', // Bulgaria
|
||||
'CY', // Cyprus
|
||||
'CZ', // Czech Republic
|
||||
'DE', // Germany
|
||||
'DK', // Denmark
|
||||
'EE', // Estonia
|
||||
'ES', // Spain
|
||||
'FI', // Finland
|
||||
'FR', // France
|
||||
'GR', // Greece
|
||||
'HR', // Croatia
|
||||
'HU', // Hungary
|
||||
'IE', // Ireland
|
||||
'IT', // Italy
|
||||
'LT', // Lithuania
|
||||
'LU', // Luxembourg
|
||||
'LV', // Latvia
|
||||
'MT', // Malta
|
||||
'NL', // Netherlands
|
||||
'PL', // Poland
|
||||
'PT', // Portugal
|
||||
'RO', // Romania
|
||||
'SE', // Sweden
|
||||
'SI', // Slovenia
|
||||
'SK', // Slovakia
|
||||
];
|
||||
|
||||
private array $tax_jurisdictions = [
|
||||
// 'AT', // Austria
|
||||
// 'BE', // Belgium
|
||||
@ -144,15 +174,15 @@ class InvoiceItemSum
|
||||
return $this;
|
||||
}
|
||||
|
||||
//should we be filtering by client country here? do we need to reflect at the company <=> client level?
|
||||
// if (in_array($this->client->country->iso_3166_2, $this->tax_jurisdictions)) { //only calculate for supported tax jurisdictions
|
||||
if (in_array($this->client->company->country()->iso_3166_2, $this->tax_jurisdictions) ) { //only calculate for supported tax jurisdictions
|
||||
|
||||
nlog($this->client->company->country()->iso_3166_2);
|
||||
|
||||
$class = "App\DataMapper\Tax\\".$this->client->company->country()->iso_3166_2."\\Rule";
|
||||
|
||||
$this->rule = new $class();
|
||||
|
||||
if($this->rule->regionWithNoTaxCoverage($this->client->country->iso_3166_2))
|
||||
return $this;
|
||||
|
||||
$this->rule
|
||||
->setEntity($this->invoice)
|
||||
->init();
|
||||
|
@ -13,8 +13,6 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\Chart\ShowChartRequest;
|
||||
use App\Services\Chart\ChartService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
class ChartController extends BaseController
|
||||
{
|
||||
@ -67,14 +65,19 @@ class ChartController extends BaseController
|
||||
*/
|
||||
public function totals(ShowChartRequest $request)
|
||||
{
|
||||
$cs = new ChartService(auth()->user()->company());
|
||||
/** @var \App\Models\User auth()->user() */
|
||||
$user = auth()->user();
|
||||
$cs = new ChartService($user->company(), $user, $user->isAdmin());
|
||||
|
||||
return response()->json($cs->totals($request->input('start_date'), $request->input('end_date')), 200);
|
||||
}
|
||||
|
||||
public function chart_summary(ShowChartRequest $request)
|
||||
{
|
||||
$cs = new ChartService(auth()->user()->company());
|
||||
|
||||
/** @var \App\Models\User auth()->user() */
|
||||
$user = auth()->user();
|
||||
$cs = new ChartService($user->company(), $user, $user->isAdmin());
|
||||
|
||||
return response()->json($cs->chart_summary($request->input('start_date'), $request->input('end_date')), 200);
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ use App\Utils\Traits\Uploadable;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Str;
|
||||
use Turbo124\Beacon\Facades\LightLogs;
|
||||
|
||||
/**
|
||||
@ -417,6 +418,13 @@ class CompanyController extends BaseController
|
||||
$this->saveDocuments($request->input('documents'), $company, false);
|
||||
}
|
||||
|
||||
if($request->has('e_invoice_certificate') && !is_null($request->file("e_invoice_certificate"))){
|
||||
|
||||
$company->e_invoice_certificate = base64_encode($request->file("e_invoice_certificate")->get());
|
||||
$company->save();
|
||||
|
||||
}
|
||||
|
||||
$this->uploadLogo($request->file('company_logo'), $company, $company);
|
||||
|
||||
return $this->itemResponse($company);
|
||||
|
@ -46,74 +46,6 @@ class EmailController extends BaseController
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a template filled with entity variables.
|
||||
*
|
||||
* @param SendEmailRequest $request
|
||||
* @return Response
|
||||
*
|
||||
* @OA\Post(
|
||||
* path="/api/v1/emails",
|
||||
* operationId="sendEmailTemplate",
|
||||
* tags={"emails"},
|
||||
* summary="Sends an email for an entity",
|
||||
* description="Sends an email for an entity",
|
||||
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
|
||||
* @OA\RequestBody(
|
||||
* description="The template subject and body",
|
||||
* required=true,
|
||||
* @OA\MediaType(
|
||||
* mediaType="application/json",
|
||||
* @OA\Schema(
|
||||
* type="object",
|
||||
* @OA\Property(
|
||||
* property="subject",
|
||||
* description="The email subject",
|
||||
* type="string",
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="body",
|
||||
* description="The email body",
|
||||
* type="string",
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="entity",
|
||||
* description="The entity name",
|
||||
* type="string",
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="entity_id",
|
||||
* description="The entity_id",
|
||||
* type="string",
|
||||
* ),
|
||||
* @OA\Property(
|
||||
* property="template",
|
||||
* description="The template required",
|
||||
* type="string",
|
||||
* ),
|
||||
* )
|
||||
* )
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=200,
|
||||
* description="success",
|
||||
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
|
||||
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
|
||||
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
|
||||
* @OA\JsonContent(ref="#/components/schemas/Template"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response=422,
|
||||
* description="Validation error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
|
||||
* ),
|
||||
* @OA\Response(
|
||||
* response="default",
|
||||
* description="Unexpected Error",
|
||||
* @OA\JsonContent(ref="#/components/schemas/Error"),
|
||||
* ),
|
||||
* )
|
||||
*/
|
||||
public function send(SendEmailRequest $request)
|
||||
{
|
||||
$entity = $request->input('entity');
|
||||
|
@ -143,8 +143,8 @@ class UpdateClientRequest extends Request
|
||||
* down to the free plan setting properties which
|
||||
* are saveable
|
||||
*
|
||||
* @param object $settings
|
||||
* @return stdClass $settings
|
||||
* @param \stdClass $settings
|
||||
* @return \stdClass $settings
|
||||
*/
|
||||
private function filterSaveableSettings($settings)
|
||||
{
|
||||
|
@ -53,7 +53,8 @@ class UpdateCompanyRequest extends Request
|
||||
$rules['country_id'] = 'integer|nullable';
|
||||
$rules['work_email'] = 'email|nullable';
|
||||
$rules['matomo_id'] = 'nullable|integer';
|
||||
|
||||
$rules['e_invoice_certificate_passphrase'] = 'sometimes|nullable';
|
||||
$rules['e_invoice_certificate'] = 'sometimes|nullable|file|mimes:p12,pfx,pem,cer,crt,der,txt,p7b,spc,bin';
|
||||
// $rules['client_registration_fields'] = 'array';
|
||||
|
||||
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
|
||||
@ -82,6 +83,10 @@ class UpdateCompanyRequest extends Request
|
||||
$input['settings'] = (array)$this->filterSaveableSettings($input['settings']);
|
||||
}
|
||||
|
||||
if(array_key_exists('e_invoice_certificate_passphrase', $input) && empty($input['e_invoice_certificate_passphrase'])) {
|
||||
unset($input['e_invoice_certificate_passphrase']);
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ class UpdateRecurringQuoteRequest extends Request
|
||||
* off / optin / optout will reset the status of this field to off to allow
|
||||
* the client to choose whether to auto_bill or not.
|
||||
*
|
||||
* @param enum $auto_bill off/always/optin/optout
|
||||
* @param string $auto_bill off/always/optin/optout
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -45,7 +45,7 @@ class StoreSchedulerRequest extends Request
|
||||
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
|
||||
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
|
||||
'parameters.entity_id' => ['bail', 'sometimes', 'string'],
|
||||
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'],
|
||||
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'],
|
||||
'parameters.date_key' => ['bail','sometimes', 'string'],
|
||||
];
|
||||
|
||||
@ -60,6 +60,7 @@ class StoreSchedulerRequest extends Request
|
||||
$this->merge(['next_run_client' => $input['next_run']]);
|
||||
}
|
||||
|
||||
return $input;
|
||||
$this->replace($input);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ class UpdateSchedulerRequest extends Request
|
||||
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
|
||||
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
|
||||
'parameters.entity_id' => ['bail', 'sometimes', 'string'],
|
||||
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_summary_report,ar_detail_report,tax_summary_report,user_sales_report,client_sales_report,client_balance_report,product_sales_report'],
|
||||
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'],
|
||||
'parameters.date_key' => ['bail','sometimes', 'string'],
|
||||
];
|
||||
|
||||
@ -57,6 +57,6 @@ class UpdateSchedulerRequest extends Request
|
||||
$this->merge(['next_run_client' => $input['next_run']]);
|
||||
}
|
||||
|
||||
return $input;
|
||||
$this->replace($input);
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ class ClientTransformer extends BaseTransformer
|
||||
public function transform($data)
|
||||
{
|
||||
if (isset($data['Company Name']) && $this->hasClient($data['Company Name'])) {
|
||||
throw new ImportException('Client already exists');
|
||||
throw new ImportException('Client already exists => '. $data['Company Name']);
|
||||
}
|
||||
|
||||
$settings = new \stdClass;
|
||||
@ -40,7 +40,7 @@ class ClientTransformer extends BaseTransformer
|
||||
|
||||
$client_id_proxy = array_key_exists('Customer ID', $data) ? 'Customer ID' : 'Primary Contact ID';
|
||||
|
||||
return [
|
||||
$data = [
|
||||
'company_id' => $this->company->id,
|
||||
'name' => $this->getString($data, 'Display Name'),
|
||||
'phone' => $this->getString($data, 'Phone'),
|
||||
@ -72,5 +72,7 @@ class ClientTransformer extends BaseTransformer
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,8 @@ class InvoiceTransformer extends BaseTransformer
|
||||
|
||||
$transformed = [
|
||||
'company_id' => $this->company->id,
|
||||
'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')),
|
||||
// 'client_id' => $this->getClient($this->getString($invoice_data, 'Customer ID'), $this->getString($invoice_data, 'Primary Contact EmailID')),
|
||||
'client_id' => $this->harvestClient($invoice_data),
|
||||
'number' => $this->getString($invoice_data, 'Invoice Number'),
|
||||
'date' => isset($invoice_data['Invoice Date']) ? date('Y-m-d', strtotime($invoice_data['Invoice Date'])) : null,
|
||||
'due_date' => isset($invoice_data['Due Date']) ? date('Y-m-d', strtotime($invoice_data['Due Date'])) : null,
|
||||
@ -80,4 +81,77 @@ class InvoiceTransformer extends BaseTransformer
|
||||
|
||||
return $transformed;
|
||||
}
|
||||
|
||||
private function harvestClient($invoice_data)
|
||||
{
|
||||
|
||||
$client_email = $this->getString($invoice_data, 'Primary Contact EmailID');
|
||||
|
||||
if (strlen($client_email) > 2) {
|
||||
$contacts = \App\Models\ClientContact::whereHas('client', function ($query) {
|
||||
$query->where('is_deleted', false);
|
||||
})
|
||||
->where('company_id', $this->company->id)
|
||||
->where('email', $client_email);
|
||||
|
||||
if ($contacts->count() >= 1) {
|
||||
return $contacts->first()->client_id;
|
||||
}
|
||||
}
|
||||
|
||||
$client_name = $this->getString($invoice_data, 'Customer Name');
|
||||
|
||||
if(strlen($client_name) >= 2) {
|
||||
$client_name_search = \App\Models\Client::where('company_id', $this->company->id)
|
||||
->where('is_deleted', false)
|
||||
->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [
|
||||
strtolower(str_replace(' ', '', $client_name)),
|
||||
]);
|
||||
|
||||
if ($client_name_search->count() >= 1) {
|
||||
return $client_name_search->first()->id;
|
||||
}
|
||||
}
|
||||
|
||||
$customer_id = $this->getString($invoice_data, 'Customer ID');
|
||||
|
||||
$client_id_search = \App\Models\Client::where('company_id', $this->company->id)
|
||||
->where('is_deleted', false)
|
||||
->where('id_number', trim($customer_id));
|
||||
|
||||
if ($client_id_search->count() >= 1) {
|
||||
return $client_id_search->first()->id;
|
||||
}
|
||||
|
||||
|
||||
$client_repository = app()->make(\App\Repositories\ClientRepository::class);
|
||||
$client_repository->import_mode = true;
|
||||
|
||||
$client = $client_repository->save(
|
||||
[
|
||||
'name' => $client_name,
|
||||
'contacts' => [
|
||||
[
|
||||
'first_name' => $client_name,
|
||||
'email' => $client_email,
|
||||
],
|
||||
],
|
||||
'address1' => $this->getString($invoice_data, 'Billing Address'),
|
||||
'city' => $this->getString($invoice_data, 'Billing City'),
|
||||
'state' => $this->getString($invoice_data, 'Billing State'),
|
||||
'postal_code' => $this->getString($invoice_data, 'Billing Code'),
|
||||
'country_id' => $this->getCountryId($this->getString($invoice_data, 'Billing Country')),
|
||||
],
|
||||
|
||||
\App\Factory\ClientFactory::create(
|
||||
$this->company->id,
|
||||
$this->company->owner()->id
|
||||
)
|
||||
);
|
||||
|
||||
$client_repository = null;
|
||||
|
||||
return $client->id;
|
||||
|
||||
}
|
||||
}
|
||||
|
78
app/Jobs/Client/UpdateTaxData.php
Normal file
78
app/Jobs/Client/UpdateTaxData.php
Normal file
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Jobs\Client;
|
||||
|
||||
use App\DataProviders\USStates;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Libraries\MultiDB;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class UpdateTaxData implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
use MakesHash;
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @param Client $client
|
||||
* @param Company $company
|
||||
*/
|
||||
public function __construct(public Client $client, protected Company $company)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
MultiDB::setDb($this->company->db);
|
||||
|
||||
if(!config('services.tax.zip_tax.key'))
|
||||
return;
|
||||
|
||||
$tax_provider = new \App\Services\Tax\Providers\TaxProvider($this->company, $this->client);
|
||||
|
||||
try {
|
||||
|
||||
$tax_provider->updateClientTaxData();
|
||||
|
||||
|
||||
if (!$this->client->state && $this->client->postal_code) {
|
||||
|
||||
$this->client->state = USStates::getState($this->client->postal_code);
|
||||
|
||||
$this->client->save();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}catch(\Exception $e){
|
||||
nlog("problem getting tax data => ".$e->getMessage());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -13,12 +13,14 @@ namespace App\Jobs\Company;
|
||||
|
||||
use App\Utils\Ninja;
|
||||
use App\Models\Company;
|
||||
use App\Models\Country;
|
||||
use App\Libraries\MultiDB;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use App\DataMapper\Tax\TaxModel;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\DataMapper\ClientRegistrationFields;
|
||||
use App\Factory\TaxRateFactory;
|
||||
|
||||
class CreateCompany
|
||||
{
|
||||
@ -53,6 +55,10 @@ class CreateCompany
|
||||
|
||||
$settings->name = isset($this->request['name']) ? $this->request['name'] : '';
|
||||
|
||||
if($country_id = $this->resolveCountry()){
|
||||
$settings->country_id = $country_id;
|
||||
}
|
||||
|
||||
$company = new Company();
|
||||
$company->account_id = $this->account->id;
|
||||
$company->company_key = $this->createHash();
|
||||
@ -74,8 +80,135 @@ class CreateCompany
|
||||
$company->subdomain = '';
|
||||
}
|
||||
|
||||
$company->save();
|
||||
/** Location Specific Configuration */
|
||||
match($settings->country_id) {
|
||||
'724' => $company = $this->spanishSetup($company),
|
||||
'36' => $company = $this->australiaSetup($company),
|
||||
default => $company->save(),
|
||||
};
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve Country
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function resolveCountry(): string
|
||||
{
|
||||
try{
|
||||
|
||||
$ip = request()->ip();
|
||||
|
||||
if(request()->hasHeader('cf-ipcountry')){
|
||||
|
||||
$c = Country::where('iso_3166_2', request()->header('cf-ipcountry'))->first();
|
||||
|
||||
if($c)
|
||||
return (string)$c->id;
|
||||
|
||||
}
|
||||
|
||||
$details = json_decode(file_get_contents("http://ip-api.com/json/{$ip}"));
|
||||
|
||||
if($details && property_exists($details, 'countryCode')){
|
||||
|
||||
$c = Country::where('iso_3166_2', $details->countryCode)->first();
|
||||
|
||||
if($c)
|
||||
return (string)$c->id;
|
||||
|
||||
}
|
||||
}
|
||||
catch(\Exception $e){
|
||||
nlog("Could not resolve country => {$e->getMessage()}");
|
||||
}
|
||||
|
||||
return '840';
|
||||
|
||||
}
|
||||
|
||||
private function spanishSetup(Company $company): Company
|
||||
{
|
||||
try {
|
||||
|
||||
$custom_fields = new \stdClass;
|
||||
$custom_fields->contact1 = "Rol|CONTABLE,FISCAL,GESTOR,RECEPTOR,TRAMITADOR,PAGADOR,PROPONENTE,B2B_FISCAL,B2B_PAYER,B2B_BUYER,B2B_COLLECTOR,B2B_SELLER,B2B_PAYMENT_RECEIVER,B2B_COLLECTION_RECEIVER,B2B_ISSUER";
|
||||
$custom_fields->contact2 = "Code|single_line_text";
|
||||
$custom_fields->contact3 = "Nombre|single_line_text";
|
||||
$custom_fields->client1 = "Administración Pública|switch";
|
||||
|
||||
$company->custom_fields = $custom_fields;
|
||||
$company->enabled_item_tax_rates = 1;
|
||||
|
||||
$settings = $company->settings;
|
||||
$settings->language_id = '7';
|
||||
$settings->e_invoice_type = 'Facturae_3.2.2';
|
||||
$settings->currency_id = '3';
|
||||
$settings->timezone_id = '42';
|
||||
|
||||
$company->settings = $settings;
|
||||
|
||||
$company->save();
|
||||
|
||||
$user = $this->account->users()->orderBy('id','asc')->first();
|
||||
|
||||
$tax_rate = TaxRateFactory::create($company->id, $user->id);
|
||||
$tax_rate->name = $company->tax_data->regions->EU->subregions->ES->tax_name;
|
||||
$tax_rate->rate = $company->tax_data->regions->EU->subregions->ES->tax_rate;
|
||||
$tax_rate->save();
|
||||
|
||||
return $company;
|
||||
|
||||
}
|
||||
catch(\Exception $e){
|
||||
nlog("SETUP: could not complete setup for Spanish Locale");
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
return $company;
|
||||
|
||||
}
|
||||
|
||||
private function australiaSetup(Company $company): Company
|
||||
{
|
||||
try {
|
||||
|
||||
$company->enabled_item_tax_rates = 1;
|
||||
$company->enabled_tax_rates = 1;
|
||||
|
||||
$translations = new \stdClass;
|
||||
$translations->invoice = "Tax Invoice";
|
||||
|
||||
$settings = $company->settings;
|
||||
$settings->currency_id = '12';
|
||||
$settings->timezone_id = '109';
|
||||
$settings->translations = $translations;
|
||||
|
||||
$company->settings = $settings;
|
||||
|
||||
$company->save();
|
||||
|
||||
$user = $company->account->users()->first();
|
||||
|
||||
$tax_rate = TaxRateFactory::create($company->id, $user->id);
|
||||
$tax_rate->name = $company->tax_data->regions->AU->subregions->AU->tax_name;
|
||||
$tax_rate->rate = $company->tax_data->regions->AU->subregions->AU->tax_rate;
|
||||
$tax_rate->save();
|
||||
|
||||
return $company;
|
||||
|
||||
}
|
||||
catch(\Exception $e){
|
||||
nlog("SETUP: could not complete setup for Spanish Locale");
|
||||
}
|
||||
|
||||
$company->save();
|
||||
|
||||
return $company;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -150,23 +150,6 @@ class EmailEntity implements ShouldQueue
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* @unused
|
||||
*/
|
||||
// private function entityEmailFailed($message)
|
||||
// {
|
||||
// switch ($this->entity_string) {
|
||||
// case 'invoice':
|
||||
// event(new InvoiceWasEmailedAndFailed($this->invitation, $this->company, $message, $this->reminder_template, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
|
||||
// break;
|
||||
|
||||
// default:
|
||||
// // code...
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
/* Builds the email builder object */
|
||||
private function resolveEmailBuilder()
|
||||
{
|
||||
|
@ -63,20 +63,6 @@ class InvoiceEmailedNotification implements ShouldQueue
|
||||
if (($key = array_search('mail', $methods)) !== false) {
|
||||
unset($methods[$key]);
|
||||
|
||||
// $template = $event->template ?? '';
|
||||
|
||||
// if(isset($event->reminder)){
|
||||
|
||||
// $template = match($event->reminder){
|
||||
// 63 => 'reminder1',
|
||||
// 64 => 'reminder2',
|
||||
// 65 => 'reminder3',
|
||||
// 66 => 'endless_reminder',
|
||||
// default => ''
|
||||
// };
|
||||
|
||||
// }
|
||||
|
||||
$nmo = new NinjaMailerObject;
|
||||
$nmo->mailable = new NinjaMailer((new EntitySentObject($event->invitation, 'invoice', $event->template))->build());
|
||||
$nmo->company = $invoice->company;
|
||||
|
@ -46,12 +46,21 @@ class InvoiceReminderEmailActivity implements ShouldQueue
|
||||
|
||||
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->invitation->invoice->user_id;
|
||||
|
||||
$reminder = match($event->template){
|
||||
'reminder1' => 63,
|
||||
'reminder2' => 64,
|
||||
'reminder3' => 65,
|
||||
'reminder_endless' => 66,
|
||||
'endless_reminder' => 66,
|
||||
default => 6,
|
||||
};
|
||||
|
||||
$fields->user_id = $user_id;
|
||||
$fields->invoice_id = $event->invitation->invoice_id;
|
||||
$fields->company_id = $event->invitation->company_id;
|
||||
$fields->client_contact_id = $event->invitation->client_contact_id;
|
||||
$fields->client_id = $event->invitation->invoice->client_id;
|
||||
$fields->activity_type_id = $event->reminder;
|
||||
$fields->activity_type_id = $reminder;
|
||||
|
||||
$this->activity_repo->save($fields, $event->invitation, $event->event_vars);
|
||||
}
|
||||
|
@ -11,18 +11,19 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\Models\Presenters\CompanyPresenter;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use App\Utils\Ninja;
|
||||
use App\Casts\EncryptedCast;
|
||||
use App\Utils\Traits\AppSetup;
|
||||
use App\Utils\Traits\CompanySettingsSaver;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Laracasts\Presenter\PresentableTrait;
|
||||
use App\Utils\Traits\CompanySettingsSaver;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use App\Models\Presenters\CompanyPresenter;
|
||||
use App\Services\Notification\NotificationService;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\Company
|
||||
@ -339,6 +340,7 @@ class Company extends BaseModel
|
||||
'notify_vendor_when_paid',
|
||||
'calculate_taxes',
|
||||
'tax_data',
|
||||
'e_invoice_certificate_passphrase',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -357,6 +359,8 @@ class Company extends BaseModel
|
||||
'deleted_at' => 'timestamp',
|
||||
'client_registration_fields' => 'array',
|
||||
'tax_data' => 'object',
|
||||
'origin_tax_data' => 'object',
|
||||
'e_invoice_certificate_passphrase' => EncryptedCast::class,
|
||||
];
|
||||
|
||||
protected $with = [];
|
||||
@ -365,7 +369,6 @@ class Company extends BaseModel
|
||||
self::ENTITY_RECURRING_INVOICE => 1,
|
||||
self::ENTITY_CREDIT => 2,
|
||||
self::ENTITY_QUOTE => 4,
|
||||
// @phpstan-ignore-next-line
|
||||
self::ENTITY_TASK => 8,
|
||||
self::ENTITY_EXPENSE => 16,
|
||||
self::ENTITY_PROJECT => 32,
|
||||
|
@ -824,7 +824,7 @@ class Invoice extends BaseModel
|
||||
case 'custom1':
|
||||
case 'custom2':
|
||||
case 'custom3':
|
||||
event(new InvoiceReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template));
|
||||
event(new InvoiceWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $template));
|
||||
break;
|
||||
default:
|
||||
// code...
|
||||
|
@ -11,9 +11,10 @@
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Jobs\Util\WebhookHandler;
|
||||
use App\Models\Client;
|
||||
use App\Models\Webhook;
|
||||
use App\Jobs\Util\WebhookHandler;
|
||||
use App\Jobs\Client\UpdateTaxData;
|
||||
|
||||
class ClientObserver
|
||||
{
|
||||
@ -27,6 +28,11 @@ class ClientObserver
|
||||
*/
|
||||
public function created(Client $client)
|
||||
{
|
||||
|
||||
if ($client->country_id == 840 && $client->company->calculate_taxes) {
|
||||
UpdateTaxData::dispatch($client, $client->company);
|
||||
}
|
||||
|
||||
$subscriptions = Webhook::where('company_id', $client->company_id)
|
||||
->where('event_id', Webhook::EVENT_CREATE_CLIENT)
|
||||
->exists();
|
||||
@ -44,6 +50,11 @@ class ClientObserver
|
||||
*/
|
||||
public function updated(Client $client)
|
||||
{
|
||||
if($client->getOriginal('postal_code') != $client->postal_code && $client->country_id == 840 && $client->company->calculate_taxes)
|
||||
{
|
||||
UpdateTaxData::dispatch($client, $client->company);
|
||||
}
|
||||
|
||||
$event = Webhook::EVENT_UPDATE_CLIENT;
|
||||
|
||||
if ($client->getOriginal('deleted_at') && !$client->deleted_at) {
|
||||
@ -54,7 +65,6 @@ class ClientObserver
|
||||
$event = Webhook::EVENT_DELETE_CLIENT;
|
||||
}
|
||||
|
||||
|
||||
$subscriptions = Webhook::where('company_id', $client->company_id)
|
||||
->where('event_id', $event)
|
||||
->exists();
|
||||
|
@ -40,6 +40,12 @@ class CompanyObserver
|
||||
//fire event to build new custom portal domain
|
||||
\Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain');
|
||||
}
|
||||
|
||||
// if($company->wasChanged()) {
|
||||
// nlog("updated event");
|
||||
// nlog($company->getChanges());
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -43,7 +43,7 @@ class Charge
|
||||
* Create a charge against a payment method.
|
||||
* @param ClientGatewayToken $cgt
|
||||
* @param PaymentHash $payment_hash
|
||||
* @return bool success/failure
|
||||
* @return mixed success/failure
|
||||
* @throws \Laracasts\Presenter\Exceptions\PresenterException
|
||||
*/
|
||||
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
|
||||
@ -86,7 +86,7 @@ class Charge
|
||||
$data['off_session'] = true;
|
||||
}
|
||||
|
||||
$response = $this->stripe->createPaymentIntent($data, array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)]));
|
||||
$response = $this->stripe->createPaymentIntent($data);
|
||||
|
||||
SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company);
|
||||
} catch (\Exception $e) {
|
||||
|
@ -777,6 +777,7 @@ class StripePaymentDriver extends BaseDriver
|
||||
->where('token', $request->data['object']['payment_method'])
|
||||
->first();
|
||||
|
||||
if($clientgateway)
|
||||
$clientgateway->delete();
|
||||
|
||||
return response()->json([], 200);
|
||||
|
@ -78,6 +78,14 @@ class RouteServiceProvider extends ServiceProvider
|
||||
}
|
||||
});
|
||||
|
||||
RateLimiter::for('404', function (Request $request) {
|
||||
if (Ninja::isSelfHost()) {
|
||||
return Limit::none();
|
||||
} else {
|
||||
return Limit::perMinute(25)->by($request->ip());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,20 +23,26 @@ trait ChartQueries
|
||||
*/
|
||||
public function getExpenseQuery($start_date, $end_date)
|
||||
{
|
||||
return DB::select(DB::raw('
|
||||
$user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT sum(expenses.amount) as amount,
|
||||
IFNULL(expenses.currency_id, :company_currency) as currency_id
|
||||
FROM expenses
|
||||
WHERE expenses.is_deleted = 0
|
||||
AND expenses.company_id = :company_id
|
||||
AND (expenses.date BETWEEN :start_date AND :end_date)
|
||||
{$user_filter}
|
||||
GROUP BY currency_id
|
||||
'), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]);
|
||||
"), ['company_currency' => $this->company->settings->currency_id, 'company_id' => $this->company->id, 'start_date' => $start_date, 'end_date' => $end_date]);
|
||||
}
|
||||
|
||||
public function getExpenseChartQuery($start_date, $end_date, $currency_id)
|
||||
{
|
||||
return DB::select(DB::raw('
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND expenses.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(expenses.amount) as total,
|
||||
expenses.date,
|
||||
@ -45,9 +51,10 @@ trait ChartQueries
|
||||
WHERE (expenses.date BETWEEN :start_date AND :end_date)
|
||||
AND expenses.company_id = :company_id
|
||||
AND expenses.is_deleted = 0
|
||||
{$user_filter}
|
||||
GROUP BY expenses.date
|
||||
HAVING currency_id = :currency_id
|
||||
'), [
|
||||
"), [
|
||||
'company_currency' => $this->company->settings->currency_id,
|
||||
'currency_id' => $currency_id,
|
||||
'company_id' => $this->company->id,
|
||||
@ -61,15 +68,19 @@ trait ChartQueries
|
||||
*/
|
||||
public function getPaymentQuery($start_date, $end_date)
|
||||
{
|
||||
return DB::select(DB::raw('
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT sum(payments.amount) as amount,
|
||||
IFNULL(payments.currency_id, :company_currency) as currency_id
|
||||
FROM payments
|
||||
WHERE payments.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND payments.company_id = :company_id
|
||||
AND (payments.date BETWEEN :start_date AND :end_date)
|
||||
GROUP BY currency_id
|
||||
'), [
|
||||
"), [
|
||||
'company_currency' => $this->company->settings->currency_id,
|
||||
'company_id' => $this->company->id,
|
||||
'start_date' => $start_date,
|
||||
@ -79,7 +90,10 @@ trait ChartQueries
|
||||
|
||||
public function getPaymentChartQuery($start_date, $end_date, $currency_id)
|
||||
{
|
||||
return DB::select(DB::raw('
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND payments.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(payments.amount - payments.refunded) as total,
|
||||
payments.date,
|
||||
@ -87,11 +101,12 @@ trait ChartQueries
|
||||
FROM payments
|
||||
WHERE payments.company_id = :company_id
|
||||
AND payments.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND payments.status_id IN (4,5,6)
|
||||
AND (payments.date BETWEEN :start_date AND :end_date)
|
||||
GROUP BY payments.date
|
||||
HAVING currency_id = :currency_id
|
||||
'), [
|
||||
"), [
|
||||
'company_currency' => $this->company->settings->currency_id,
|
||||
'currency_id' => $currency_id,
|
||||
'company_id' => $this->company->id,
|
||||
@ -105,6 +120,9 @@ trait ChartQueries
|
||||
*/
|
||||
public function getOutstandingQuery($start_date, $end_date)
|
||||
{
|
||||
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(invoices.balance) as amount,
|
||||
@ -116,6 +134,7 @@ trait ChartQueries
|
||||
WHERE invoices.status_id IN (2,3)
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND invoices.is_deleted = 0
|
||||
AND invoices.balance > 0
|
||||
AND (invoices.date BETWEEN :start_date AND :end_date)
|
||||
@ -125,6 +144,8 @@ trait ChartQueries
|
||||
|
||||
public function getRevenueQuery($start_date, $end_date)
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(invoices.paid_to_date) as paid_to_date,
|
||||
@ -134,6 +155,7 @@ trait ChartQueries
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND invoices.is_deleted = 0
|
||||
AND invoices.amount > 0
|
||||
AND invoices.status_id IN (3,4)
|
||||
@ -144,6 +166,8 @@ trait ChartQueries
|
||||
|
||||
public function getInvoicesQuery($start_date, $end_date)
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(invoices.amount) as invoiced_amount,
|
||||
@ -153,6 +177,7 @@ trait ChartQueries
|
||||
on invoices.client_id = clients.id
|
||||
WHERE invoices.status_id IN (2,3,4)
|
||||
AND invoices.company_id = :company_id
|
||||
{$user_filter}
|
||||
AND invoices.amount > 0
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
@ -163,6 +188,8 @@ trait ChartQueries
|
||||
|
||||
public function getOutstandingChartQuery($start_date, $end_date, $currency_id)
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(invoices.balance) as total,
|
||||
@ -175,6 +202,7 @@ trait ChartQueries
|
||||
AND invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND (invoices.date BETWEEN :start_date AND :end_date)
|
||||
GROUP BY invoices.date
|
||||
HAVING currency_id = :currency_id
|
||||
@ -190,6 +218,8 @@ trait ChartQueries
|
||||
|
||||
public function getInvoiceChartQuery($start_date, $end_date, $currency_id)
|
||||
{
|
||||
$user_filter = $this->is_admin ? '' : 'AND clients.user_id = '.$this->user->id;
|
||||
|
||||
return DB::select(DB::raw("
|
||||
SELECT
|
||||
sum(invoices.amount) as total,
|
||||
@ -201,6 +231,7 @@ trait ChartQueries
|
||||
WHERE invoices.company_id = :company_id
|
||||
AND clients.is_deleted = 0
|
||||
AND invoices.is_deleted = 0
|
||||
{$user_filter}
|
||||
AND invoices.status_id IN (2,3,4)
|
||||
AND (invoices.date BETWEEN :start_date AND :end_date)
|
||||
GROUP BY invoices.date
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace App\Services\Chart;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Expense;
|
||||
@ -20,11 +21,8 @@ class ChartService
|
||||
{
|
||||
use ChartQueries;
|
||||
|
||||
public Company $company;
|
||||
|
||||
public function __construct(Company $company)
|
||||
public function __construct(public Company $company, private User $user, private bool $is_admin)
|
||||
{
|
||||
$this->company = $company;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -37,6 +35,9 @@ class ChartService
|
||||
$currencies = Client::withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('is_deleted', 0)
|
||||
->when(!$this->is_admin, function ($query) {
|
||||
$query->where('user_id', $this->user->id);
|
||||
})
|
||||
->distinct()
|
||||
->pluck('settings->currency_id as id');
|
||||
|
||||
@ -47,6 +48,9 @@ class ChartService
|
||||
$expense_currencies = Expense::withTrashed()
|
||||
->where('company_id', $this->company->id)
|
||||
->where('is_deleted', 0)
|
||||
->when(!$this->is_admin, function ($query) {
|
||||
$query->where('user_id', $this->user->id);
|
||||
})
|
||||
->distinct()
|
||||
->pluck('currency_id as id');
|
||||
|
||||
|
@ -18,6 +18,7 @@ use josemmo\Facturae\FacturaeItem;
|
||||
use josemmo\Facturae\FacturaeParty;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use josemmo\Facturae\Common\FacturaeSigner;
|
||||
use josemmo\Facturae\FacturaeCentre;
|
||||
|
||||
class FacturaEInvoice extends AbstractService
|
||||
{
|
||||
@ -25,6 +26,24 @@ class FacturaEInvoice extends AbstractService
|
||||
|
||||
private $calc;
|
||||
|
||||
private $centre_codes = [
|
||||
'CONTABLE' => FacturaeCentre::ROLE_CONTABLE,
|
||||
'FISCAL' => FacturaeCentre::ROLE_FISCAL,
|
||||
'GESTOR' => FacturaeCentre::ROLE_GESTOR,
|
||||
'RECEPTOR' => FacturaeCentre::ROLE_RECEPTOR,
|
||||
'TRAMITADOR' => FacturaeCentre::ROLE_TRAMITADOR,
|
||||
'PAGADOR' => FacturaeCentre::ROLE_PAGADOR,
|
||||
'PROPONENTE' => FacturaeCentre::ROLE_PAGADOR,
|
||||
'B2B_FISCAL' => FacturaeCentre::ROLE_B2B_FISCAL,
|
||||
'B2B_PAYER' => FacturaeCentre::ROLE_B2B_PAYER,
|
||||
'B2B_BUYER' => FacturaeCentre::ROLE_B2B_BUYER,
|
||||
'B2B_COLLECTOR' => FacturaeCentre::ROLE_B2B_COLLECTOR,
|
||||
'B2B_SELLER' => FacturaeCentre::ROLE_B2B_SELLER,
|
||||
'B2B_PAYMENT_RECEIVER' => FacturaeCentre::ROLE_B2B_PAYMENT_RECEIVER ,
|
||||
'B2B_COLLECTION_RECEIVER' => FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER ,
|
||||
'B2B_ISSUER' => FacturaeCentre::ROLE_B2B_ISSUER,
|
||||
];
|
||||
|
||||
// Facturae::SCHEMA_3_2 Invoice Format 3.2
|
||||
// Facturae::SCHEMA_3_2_1 Invoice Format 3.2.1
|
||||
// Facturae::SCHEMA_3_2_2 Invoice Format 3.2.2
|
||||
@ -111,6 +130,25 @@ class FacturaEInvoice extends AbstractService
|
||||
// FacturaeCentre::ROLE_B2B_COLLECTION_RECEIVER Collection receiver in FACeB2B
|
||||
// FacturaeCentre::ROLE_B2B_ISSUER Issuer in FACeB2B
|
||||
|
||||
/*
|
||||
const ROLE_CONTABLE = "01";
|
||||
const ROLE_FISCAL = "01";
|
||||
const ROLE_GESTOR = "02";
|
||||
const ROLE_RECEPTOR = "02";
|
||||
const ROLE_TRAMITADOR = "03";
|
||||
const ROLE_PAGADOR = "03";
|
||||
const ROLE_PROPONENTE = "04";
|
||||
|
||||
const ROLE_B2B_FISCAL = "Fiscal";
|
||||
const ROLE_B2B_PAYER = "Payer";
|
||||
const ROLE_B2B_BUYER = "Buyer";
|
||||
const ROLE_B2B_COLLECTOR = "Collector";
|
||||
const ROLE_B2B_SELLER = "Seller";
|
||||
const ROLE_B2B_PAYMENT_RECEIVER = "Payment receiver";
|
||||
const ROLE_B2B_COLLECTION_RECEIVER = "Collection receiver";
|
||||
const ROLE_B2B_ISSUER = "Issuer";
|
||||
*/
|
||||
|
||||
|
||||
public function __construct(public Invoice $invoice, private mixed $profile)
|
||||
{
|
||||
@ -146,6 +184,33 @@ class FacturaEInvoice extends AbstractService
|
||||
|
||||
}
|
||||
|
||||
/** Check if this is a public administration body */
|
||||
private function setFace(): array
|
||||
{
|
||||
$facturae_centres = [];
|
||||
|
||||
if($this->invoice->client->custom_value1 == 'yes')
|
||||
{
|
||||
|
||||
foreach($this->invoice->client->contacts() as $contact)
|
||||
{
|
||||
|
||||
if(in_array($contact->custom_value1, array_keys($this->centre_codes)))
|
||||
{
|
||||
$facturae_centres[] = new FacturaeCentre([
|
||||
'role' => $this->centre_codes[$contact->custom_value1],
|
||||
'code' => $contact->custom_value2,
|
||||
'name' => $contact->custom_value3,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $facturae_centres;
|
||||
}
|
||||
|
||||
private function setPoNumber(): self
|
||||
{
|
||||
if(strlen($this->invoice->po_number) > 1) {
|
||||
@ -280,6 +345,7 @@ class FacturaEInvoice extends AbstractService
|
||||
"fax" => "",
|
||||
"website" => substr($company->settings->website, 0, 50),
|
||||
"contactPeople" => substr($company->owner()->present()->name(), 0, 40),
|
||||
'centres' => $this->setFace(),
|
||||
// "cnoCnae" => "04647", // Clasif. Nacional de Act. Económicas
|
||||
// "ineTownCode" => "280796" // Cód. de municipio del INE
|
||||
]);
|
||||
|
@ -77,13 +77,13 @@ class EmailReport
|
||||
|
||||
match($this->scheduler->parameters['report_name'])
|
||||
{
|
||||
'product_sales_report' => $export = (new ProductSalesExport($this->scheduler->company, $data)),
|
||||
'email_ar_detailed_report' => $export = (new ARDetailReport($this->scheduler->company, $data)),
|
||||
'email_ar_summary_report' => $export = (new ARSummaryReport($this->scheduler->company, $data)),
|
||||
'email_tax_summary_report' => $export = (new TaxSummaryReport($this->scheduler->company, $data)),
|
||||
'email_client_balance_report' => $export = (new ClientBalanceReport($this->scheduler->company, $data)),
|
||||
'email_client_sales_report' => $export = (new ClientSalesReport($this->scheduler->company, $data)),
|
||||
'email_user_sales_report' => $export = (new UserSalesReport($this->scheduler->company, $data)),
|
||||
'product_sales' => $export = (new ProductSalesExport($this->scheduler->company, $data)),
|
||||
'ar_detailed' => $export = (new ARDetailReport($this->scheduler->company, $data)),
|
||||
'ar_summary' => $export = (new ARSummaryReport($this->scheduler->company, $data)),
|
||||
'tax_summary' => $export = (new TaxSummaryReport($this->scheduler->company, $data)),
|
||||
'client_balance' => $export = (new ClientBalanceReport($this->scheduler->company, $data)),
|
||||
'client_sales' => $export = (new ClientSalesReport($this->scheduler->company, $data)),
|
||||
'user_sales' => $export = (new UserSalesReport($this->scheduler->company, $data)),
|
||||
'clients' => $export = (new ClientExport($this->scheduler->company, $data)),
|
||||
'client_contacts' => $export = (new ContactExport($this->scheduler->company, $data)),
|
||||
'credits' => $export = (new CreditExport($this->scheduler->company, $data)),
|
||||
|
@ -52,15 +52,14 @@ class TaxProvider
|
||||
|
||||
private mixed $api_credentials;
|
||||
|
||||
public function __construct(protected Company $company, protected Client $client)
|
||||
public function __construct(public Company $company, public ?Client $client = null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function updateCompanyTaxData(): self
|
||||
{
|
||||
$this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later
|
||||
$this->configureProvider($this->provider, $this->company->country()->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later
|
||||
|
||||
$company_details = [
|
||||
'address1' => $this->company->settings->address1,
|
||||
@ -77,7 +76,7 @@ class TaxProvider
|
||||
|
||||
$tax_data = $tax_provider->run();
|
||||
|
||||
$this->company->tax_data = $tax_data;
|
||||
$this->company->origin_tax_data = $tax_data;
|
||||
|
||||
$this->company->save();
|
||||
|
||||
@ -87,7 +86,7 @@ class TaxProvider
|
||||
|
||||
public function updateClientTaxData(): self
|
||||
{
|
||||
$this->configureProvider($this->provider); //hard coded for now to one provider, but we'll be able to swap these out later
|
||||
$this->configureProvider($this->provider, $this->client->country->iso_3166_2); //hard coded for now to one provider, but we'll be able to swap these out later
|
||||
|
||||
$billing_details =[
|
||||
'address1' => $this->client->address1,
|
||||
@ -108,24 +107,24 @@ class TaxProvider
|
||||
];
|
||||
|
||||
|
||||
$tax_provider = new $this->provider();
|
||||
$tax_provider = new $this->provider($billing_details);
|
||||
|
||||
$tax_provider->setApiCredentials($this->api_credentials);
|
||||
|
||||
$tax_data = $tax_provider->run();
|
||||
|
||||
$this->company->tax_data = $tax_data;
|
||||
$this->client->tax_data = $tax_data;
|
||||
|
||||
$this->company->save();
|
||||
$this->client->save();
|
||||
|
||||
return $this;
|
||||
|
||||
}
|
||||
|
||||
private function configureProvider(?string $provider): self
|
||||
private function configureProvider(?string $provider, string $country_code): self
|
||||
{
|
||||
|
||||
match($this->client->country->iso_3166_2){
|
||||
match($country_code){
|
||||
'US' => $this->configureZipTax(),
|
||||
"AT" => $this->configureEuTax(),
|
||||
"BE" => $this->configureEuTax(),
|
||||
@ -168,11 +167,11 @@ class TaxProvider
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function noTaxRegionDefined(): self
|
||||
private function noTaxRegionDefined()
|
||||
{
|
||||
throw new \Exception("No tax region defined for this country");
|
||||
|
||||
return $this;
|
||||
// return $this;
|
||||
}
|
||||
|
||||
private function configureZipTax(): self
|
||||
|
@ -27,17 +27,21 @@ class ZipTax implements TaxProviderInterface
|
||||
|
||||
public function run()
|
||||
{
|
||||
$string_address = implode(" ", $this->address);
|
||||
|
||||
$response = $this->callApi(['key' => $this->api_key, 'address' => $this->address]);
|
||||
$response = $this->callApi(['key' => $this->api_key, 'address' => $string_address]);
|
||||
|
||||
if($response->successful())
|
||||
return $response->json();
|
||||
if($response->successful()){
|
||||
|
||||
return $this->parseResponse($response->json());
|
||||
|
||||
}
|
||||
|
||||
if(isset($this->address['postal_code'])) {
|
||||
$response = $this->callApi(['key' => $this->api_key, 'address' => $this->address['postal_code']]);
|
||||
|
||||
if($response->successful())
|
||||
return $response->json();
|
||||
return $this->parseResponse($response->json());
|
||||
|
||||
}
|
||||
|
||||
@ -65,4 +69,13 @@ class ZipTax implements TaxProviderInterface
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
private function parseResponse($response)
|
||||
{
|
||||
if(isset($response['results']['0']))
|
||||
return $response['results']['0'];
|
||||
|
||||
throw new \Exception("Error resolving tax (code) = " . $response['rCode']);
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -31,4 +31,9 @@ class TaxService
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function initTaxProvider()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -199,6 +199,8 @@ class CompanyTransformer extends EntityTransformer
|
||||
'invoice_task_hours' => (bool) $company->invoice_task_hours,
|
||||
'calculate_taxes' => (bool) $company->calculate_taxes,
|
||||
'tax_data' => $company->tax_data ?: new \stdClass,
|
||||
'has_e_invoice_certificate' => $company->e_invoice_certificate ? true : false,
|
||||
'has_e_invoice_certificate_passphrase' => $company->e_invoice_certificate_passphrase ? true : false,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,12 @@ trait CleanLineItems
|
||||
$item['tax_id'] = '1';
|
||||
}
|
||||
elseif(array_key_exists('tax_id', $item) && $item['tax_id'] == '') {
|
||||
|
||||
if($item['type_id'] == '2')
|
||||
$item['tax_id'] = '2';
|
||||
else
|
||||
$item['tax_id'] = '1';
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -219,8 +219,4 @@ return [
|
||||
'client_id' => env('SHOPIFY_CLIENT_ID', null),
|
||||
'client_secret' => env('SHOPIFY_CLIENT_SECRET', null),
|
||||
],
|
||||
'tax_api' => [
|
||||
'provider' => env('TAX_API_PROVIDER', false),
|
||||
'api_key' => env('TAX_API_KEY', false),
|
||||
]
|
||||
];
|
||||
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
|
||||
Schema::table('companies', function (Illuminate\Database\Schema\Blueprint $table) {
|
||||
$table->text('e_invoice_certificate')->nullable();
|
||||
$table->text('e_invoice_certificate_passphrase')->nullable();
|
||||
$table->text('origin_tax_data')->nullable();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
};
|
@ -20,17 +20,17 @@ $LANG = array(
|
||||
'additional_info' => 'Informació adicional',
|
||||
'payment_terms' => 'Condicions de pagament',
|
||||
'currency_id' => 'Moneda',
|
||||
'size_id' => 'Tamany de l\'empresa',
|
||||
'industry_id' => 'Industria',
|
||||
'size_id' => 'Mida de l\'empresa',
|
||||
'industry_id' => 'Sector industrial',
|
||||
'private_notes' => 'Notes privades',
|
||||
'invoice' => 'Factura',
|
||||
'client' => 'Client',
|
||||
'invoice_date' => 'Data factura',
|
||||
'due_date' => 'Data venciment',
|
||||
'invoice_number' => 'Número de factura',
|
||||
'invoice_number_short' => 'Factura #',
|
||||
'invoice_number_short' => 'Núm. factura',
|
||||
'po_number' => 'Apartat de correus',
|
||||
'po_number_short' => 'Apartat de correus #',
|
||||
'po_number_short' => 'Núm apt correus',
|
||||
'frequency_id' => 'Quant sovint',
|
||||
'discount' => 'Descompte',
|
||||
'taxes' => 'Impostos',
|
||||
@ -44,7 +44,7 @@ $LANG = array(
|
||||
'net_subtotal' => 'Net',
|
||||
'paid_to_date' => 'Pagat',
|
||||
'balance_due' => 'Pendent',
|
||||
'invoice_design_id' => 'Diseny',
|
||||
'invoice_design_id' => 'Disseny',
|
||||
'terms' => 'Condicions',
|
||||
'your_invoice' => 'La teva factura',
|
||||
'remove_contact' => 'Esborra contacte',
|
||||
@ -54,22 +54,22 @@ $LANG = array(
|
||||
'enable' => 'Activa',
|
||||
'learn_more' => 'Aprèn més',
|
||||
'manage_rates' => 'Administrar tarifes',
|
||||
'note_to_client' => 'Nota al client',
|
||||
'invoice_terms' => 'Condicions factura',
|
||||
'note_to_client' => 'Nota per al client',
|
||||
'invoice_terms' => 'Condicions de la factura',
|
||||
'save_as_default_terms' => 'Guarda com a condicions per defecte',
|
||||
'download_pdf' => 'Descarrega PDF',
|
||||
'pay_now' => 'Paga ara',
|
||||
'save_invoice' => 'Guarda factura',
|
||||
'clone_invoice' => 'Clonar a fatura',
|
||||
'archive_invoice' => 'Arxivar factura',
|
||||
'clone_invoice' => 'Clona a fatura',
|
||||
'archive_invoice' => 'Arxiva factura',
|
||||
'delete_invoice' => 'Suprimex factura',
|
||||
'email_invoice' => 'Enviar factura per correu electrónic',
|
||||
'enter_payment' => 'Introduir pagament',
|
||||
'email_invoice' => 'Envia factura per correu electrònic',
|
||||
'enter_payment' => 'Introdueix pagament',
|
||||
'tax_rates' => 'Impostos',
|
||||
'rate' => 'Preu',
|
||||
'settings' => 'Paràmetres',
|
||||
'enable_invoice_tax' => 'Activar especificar <b>impost</b>',
|
||||
'enable_line_item_tax' => 'Activar especificar <b>impost per línea</b>',
|
||||
'enable_invoice_tax' => 'Activa especificar <b>impost</b>',
|
||||
'enable_line_item_tax' => 'Activa especificar <b>impost per línea</b>',
|
||||
'dashboard' => 'Tauler de control',
|
||||
'dashboard_totals_in_all_currencies_help' => 'Nota: afegiu un :link anomenat ":name" per mostrar els totals utilitzant una moneda base única.',
|
||||
'clients' => 'Clients',
|
||||
@ -83,28 +83,33 @@ $LANG = array(
|
||||
'company_details' => 'Detalls de l\'empresa',
|
||||
'online_payments' => 'Pagaments en línia',
|
||||
'notifications' => 'Notificacions',
|
||||
'import_export' => 'Importar | Exportar',
|
||||
'import_export' => 'Importació | Exportació',
|
||||
'done' => 'Fet',
|
||||
'save' => 'Desa',
|
||||
'create' => 'Crear',
|
||||
'upload' => 'Penjar',
|
||||
'import' => 'Importar',
|
||||
'download' => 'Baixar',
|
||||
'cancel' => 'Cancel·lar',
|
||||
'close' => 'Tancar',
|
||||
'create' => 'Crea',
|
||||
'upload' => 'Penja',
|
||||
'import' => 'Importa',
|
||||
'download' => 'Baixa',
|
||||
'cancel' => 'Cancel·la',
|
||||
'close' => 'Tanca',
|
||||
'provide_email' => 'Si us plau, indica una adreça de correu electrònic vàlida',
|
||||
'powered_by' => 'Funciona amb',
|
||||
'no_items' => 'No hi ha conceptes',
|
||||
'no_items' => 'No hi ha cap element',
|
||||
'recurring_invoices' => 'Factures recurrents',
|
||||
'recurring_help' => '<p>Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.</p>
|
||||
<p>Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1 </p>
|
||||
',
|
||||
<p>Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1 </p>
|
||||
<p>Exemples de variables dinàmiques de factures:</p>
|
||||
<ul>
|
||||
<li>"Quota gimnàs pel mes de :MONTH" >> "Quota gimnàs pel mes de juliol"</li>
|
||||
<li>"Subscripció anual :YEAR+1" >> "Subscripció anual 2015"</li>
|
||||
<li>"Pagament consultor del :QUARTER+1" >> "Pagament consultor del Q2"</li>
|
||||
</ul>',
|
||||
'recurring_quotes' => 'Pressupostos recurrents',
|
||||
'in_total_revenue' => 'en ingressos totals',
|
||||
'billed_client' => 'client facturat',
|
||||
'billed_clients' => 'clients facturats',
|
||||
'active_client' => 'Client actiu',
|
||||
'active_clients' => 'Clients actius',
|
||||
'active_client' => 'client actiu',
|
||||
'active_clients' => 'clients actius',
|
||||
'invoices_past_due' => 'Factures vençudes',
|
||||
'upcoming_invoices' => 'Properes factures',
|
||||
'average_invoice' => 'Mitjana de facturació',
|
||||
@ -2407,7 +2412,7 @@ $LANG = array(
|
||||
'currency_vanuatu_vatu' => 'Vanuatu Vatu',
|
||||
|
||||
'currency_cuban_peso' => 'Cuban Peso',
|
||||
'currency_bz_dollar' => 'BZ Dollar',
|
||||
'currency_bz_dollar' => 'Dòlar BZ',
|
||||
|
||||
'review_app_help' => 'We hope you\'re enjoying using the app.<br/>If you\'d consider :link we\'d greatly appreciate it!',
|
||||
'writing_a_review' => 'escriu una ressenya',
|
||||
@ -4007,6 +4012,7 @@ $LANG = array(
|
||||
'notification_invoice_reminder1_sent_subject' => 'Reminder 1 for Invoice :invoice was sent to :client',
|
||||
'notification_invoice_reminder2_sent_subject' => 'Reminder 2 for Invoice :invoice was sent to :client',
|
||||
'notification_invoice_reminder3_sent_subject' => 'Reminder 3 for Invoice :invoice was sent to :client',
|
||||
'notification_invoice_custom_sent_subject' => 'Custom reminder for Invoice :invoice was sent to :client',
|
||||
'notification_invoice_reminder_endless_sent_subject' => 'Endless reminder for Invoice :invoice was sent to :client',
|
||||
'assigned_user' => 'Assigned User',
|
||||
'setup_steps_notice' => 'To proceed to next step, make sure you test each section.',
|
||||
@ -4255,9 +4261,9 @@ $LANG = array(
|
||||
'klarna' => 'Klarna',
|
||||
'eps' => 'EPS',
|
||||
'becs' => 'BECS Direct Debit',
|
||||
'bacs' => 'BACS Direct Debit',
|
||||
'payment_type_BACS' => 'BACS Direct Debit',
|
||||
'missing_payment_method' => 'Please add a payment method first, before trying to pay.',
|
||||
'bacs' => 'Dèbit directe BACS',
|
||||
'payment_type_BACS' => 'Dèbit directe BACS',
|
||||
'missing_payment_method' => 'Introduïu un sistema de pagament primer, abans d\'intentar pagar.',
|
||||
'becs_mandate' => 'By providing your bank account details, you agree to this <a class="underline" href="https://stripe.com/au-becs-dd-service-agreement/legal">Direct Debit Request and the Direct Debit Request service agreement</a>, and authorise Stripe Payments Australia Pty Ltd ACN 160 180 343 Direct Debit User ID number 507156 (“Stripe”) to debit your account through the Bulk Electronic Clearing System (BECS) on behalf of :company (the “Merchant”) for any amounts separately communicated to you by the Merchant. You certify that you are either an account holder or an authorised signatory on the account listed above.',
|
||||
'you_need_to_accept_the_terms_before_proceeding' => 'You need to accept the terms before proceeding.',
|
||||
'direct_debit' => 'Direct Debit',
|
||||
@ -4384,7 +4390,7 @@ $LANG = array(
|
||||
'imported_customers' => 'Successfully started importing customers',
|
||||
'login_success' => 'Successful Login',
|
||||
'login_failure' => 'Failed Login',
|
||||
'exported_data' => 'Once the file is ready you\'ll receive an email with a download link',
|
||||
'exported_data' => 'Quan l\'arxiu estigui llest rebreu un missatge de correu amb l\'enllaç de descàrrega',
|
||||
'include_deleted_clients' => 'Include Deleted Clients',
|
||||
'include_deleted_clients_help' => 'Load records belonging to deleted clients',
|
||||
'step_1_sign_in' => 'Step 1: Sign In',
|
||||
@ -4473,7 +4479,7 @@ $LANG = array(
|
||||
'activity_123' => ':user deleted recurring expense :recurring_expense',
|
||||
'activity_124' => ':user restored recurring expense :recurring_expense',
|
||||
'fpx' => "FPX",
|
||||
'to_view_entity_set_password' => 'To view the :entity you need to set a password.',
|
||||
'to_view_entity_set_password' => 'Per a veure :entity haureu d\'establir una contrasenya.',
|
||||
'unsubscribe' => 'Unsubscribe',
|
||||
'unsubscribed' => 'Unsubscribed',
|
||||
'unsubscribed_text' => 'You have been removed from notifications for this document',
|
||||
@ -4571,7 +4577,7 @@ $LANG = array(
|
||||
'purchase_order_number' => 'Purchase Order Number',
|
||||
'purchase_order_number_short' => 'Purchase Order #',
|
||||
'inventory_notification_subject' => 'Inventory threshold notification for product: :product',
|
||||
'inventory_notification_body' => 'Threshold of :amount has been reached for product: :product',
|
||||
'inventory_notification_body' => 'S\'ha arribat al límit de :amount per al producte :product ',
|
||||
'activity_130' => ':user created purchase order :purchase_order',
|
||||
'activity_131' => ':user updated purchase order :purchase_order',
|
||||
'activity_132' => ':user archived purchase order :purchase_order',
|
||||
@ -4603,7 +4609,7 @@ $LANG = array(
|
||||
'vendor_document_upload' => 'Vendor Document Upload',
|
||||
'vendor_document_upload_help' => 'Enable vendors to upload documents',
|
||||
'are_you_enjoying_the_app' => 'Are you enjoying the app?',
|
||||
'yes_its_great' => 'Yes, it\'s great!',
|
||||
'yes_its_great' => 'Sí, genial!',
|
||||
'not_so_much' => 'Not so much',
|
||||
'would_you_rate_it' => 'Great to hear! Would you like to rate it?',
|
||||
'would_you_tell_us_more' => 'Sorry to hear it! Would you like to tell us more?',
|
||||
@ -4908,75 +4914,75 @@ $LANG = array(
|
||||
'update_payment' => 'Update Payment',
|
||||
'markup' => 'Markup',
|
||||
'unlock_pro' => 'Unlock Pro',
|
||||
'upgrade_to_paid_plan_to_schedule' => 'Upgrade to a paid plan to create schedules',
|
||||
'next_run' => 'Next Run',
|
||||
'all_clients' => 'All Clients',
|
||||
'show_aging_table' => 'Show Aging Table',
|
||||
'show_payments_table' => 'Show Payments Table',
|
||||
'upgrade_to_paid_plan_to_schedule' => 'Actualitzeu a un pla de pagament per a crear calendaris',
|
||||
'next_run' => 'Següent volta',
|
||||
'all_clients' => 'Tots els clients',
|
||||
'show_aging_table' => 'Veure taula de compliment',
|
||||
'show_payments_table' => 'Veure taula de pagaments',
|
||||
'email_statement' => 'Email Statement',
|
||||
'once' => 'Once',
|
||||
'schedules' => 'Schedules',
|
||||
'new_schedule' => 'New Schedule',
|
||||
'edit_schedule' => 'Edit Schedule',
|
||||
'created_schedule' => 'Successfully created schedule',
|
||||
'updated_schedule' => 'Successfully updated schedule',
|
||||
'archived_schedule' => 'Successfully archived schedule',
|
||||
'deleted_schedule' => 'Successfully deleted schedule',
|
||||
'removed_schedule' => 'Successfully removed schedule',
|
||||
'restored_schedule' => 'Successfully restored schedule',
|
||||
'search_schedule' => 'Search Schedule',
|
||||
'search_schedules' => 'Search Schedules',
|
||||
'update_product' => 'Update Product',
|
||||
'create_purchase_order' => 'Create Purchase Order',
|
||||
'update_purchase_order' => 'Update Purchase Order',
|
||||
'sent_invoice' => 'Sent Invoice',
|
||||
'sent_quote' => 'Sent Quote',
|
||||
'sent_credit' => 'Sent Credit',
|
||||
'sent_purchase_order' => 'Sent Purchase Order',
|
||||
'image_url' => 'Image URL',
|
||||
'max_quantity' => 'Max Quantity',
|
||||
'test_url' => 'Test URL',
|
||||
'auto_bill_help_off' => 'Option is not shown',
|
||||
'auto_bill_help_optin' => 'Option is shown but not selected',
|
||||
'auto_bill_help_optout' => 'Option is shown and selected',
|
||||
'auto_bill_help_always' => 'Option is not shown',
|
||||
'view_all' => 'View All',
|
||||
'edit_all' => 'Edit All',
|
||||
'accept_purchase_order_number' => 'Accept Purchase Order Number',
|
||||
'accept_purchase_order_number_help' => 'Enable clients to provide a PO number when approving a quote',
|
||||
'from_email' => 'From Email',
|
||||
'show_preview' => 'Show Preview',
|
||||
'show_paid_stamp' => 'Show Paid Stamp',
|
||||
'show_shipping_address' => 'Show Shipping Address',
|
||||
'no_documents_to_download' => 'There are no documents in the selected records to download',
|
||||
'pixels' => 'Pixels',
|
||||
'logo_size' => 'Logo Size',
|
||||
'failed' => 'Failed',
|
||||
'client_contacts' => 'Client Contacts',
|
||||
'sync_from' => 'Sync From',
|
||||
'gateway_payment_text' => 'Invoices: :invoices for :amount for client :client',
|
||||
'gateway_payment_text_no_invoice' => 'Payment with no invoice for amount :amount for client :client',
|
||||
'click_to_variables' => 'Client here to see all variables.',
|
||||
'ship_to' => 'Ship to',
|
||||
'stripe_direct_debit_details' => 'Please transfer into the nominated bank account above.',
|
||||
'branch_name' => 'Branch Name',
|
||||
'branch_code' => 'Branch Code',
|
||||
'bank_name' => 'Bank Name',
|
||||
'bank_code' => 'Bank Code',
|
||||
'once' => 'Una volta',
|
||||
'schedules' => 'Calendaris',
|
||||
'new_schedule' => 'Nou calendari',
|
||||
'edit_schedule' => 'Edita calendari',
|
||||
'created_schedule' => 'Calendari creat correctament',
|
||||
'updated_schedule' => 'Calendari editat correctament',
|
||||
'archived_schedule' => 'Calendari arxivat correctament',
|
||||
'deleted_schedule' => 'Calendari esborrat correctament',
|
||||
'removed_schedule' => 'Calendari eliminat correctament ',
|
||||
'restored_schedule' => 'Calendari restaurat correctament',
|
||||
'search_schedule' => 'Cerca calendari',
|
||||
'search_schedules' => 'Cerca calendaris',
|
||||
'update_product' => 'Actualitza producte',
|
||||
'create_purchase_order' => 'Crea ordre de compra',
|
||||
'update_purchase_order' => 'Actualitza ordre de compra',
|
||||
'sent_invoice' => 'Factura enviada',
|
||||
'sent_quote' => 'Pressupost enviat',
|
||||
'sent_credit' => 'Crèdit enviat',
|
||||
'sent_purchase_order' => 'Ordre de compra enviada',
|
||||
'image_url' => 'URL de la imatge',
|
||||
'max_quantity' => 'Quantitat màxims',
|
||||
'test_url' => 'URL de prova',
|
||||
'auto_bill_help_off' => 'L\'opció no es mostra',
|
||||
'auto_bill_help_optin' => 'L\'opció es mostra però no se selecciona',
|
||||
'auto_bill_help_optout' => 'L\'opció es mostra i selecciona',
|
||||
'auto_bill_help_always' => 'L\'opció no es mostra',
|
||||
'view_all' => 'Mostra-ho tot',
|
||||
'edit_all' => 'Edita-ho tot',
|
||||
'accept_purchase_order_number' => 'Accepta el número d\'ordre de compra',
|
||||
'accept_purchase_order_number_help' => 'Permet als clients afegir un número d\'ordre de compra quan aprovin un pressupost',
|
||||
'from_email' => 'De correu electrònic',
|
||||
'show_preview' => 'Mostra previsualització',
|
||||
'show_paid_stamp' => 'Mostra segell de "Pagat"',
|
||||
'show_shipping_address' => 'Mostra adreça d\'enviament',
|
||||
'no_documents_to_download' => 'No hi ha cap documents a descarregar en cap dels elements selecctionats',
|
||||
'pixels' => 'Píxels',
|
||||
'logo_size' => 'Mida del logo',
|
||||
'failed' => 'Fallat',
|
||||
'client_contacts' => 'Contactes del client',
|
||||
'sync_from' => 'Sincronitza de',
|
||||
'gateway_payment_text' => 'Factures: :invoices de :amount per al client :client',
|
||||
'gateway_payment_text_no_invoice' => 'Pagament sense factura de :amount per al client :client',
|
||||
'click_to_variables' => 'Pitgeu aquí per veure totes les variables.',
|
||||
'ship_to' => 'Envia a',
|
||||
'stripe_direct_debit_details' => 'Transferiu al compte bancari especificat a dalt, si us plau.',
|
||||
'branch_name' => 'Nom de l\'oficina',
|
||||
'branch_code' => 'Codi de l\'oficina',
|
||||
'bank_name' => 'Nom del banc',
|
||||
'bank_code' => 'Codi del banc',
|
||||
'bic' => 'BIC',
|
||||
'change_plan_description' => 'Upgrade or downgrade your current plan.',
|
||||
'add_company_logo' => 'Add Logo',
|
||||
'add_stripe' => 'Add Stripe',
|
||||
'invalid_coupon' => 'Invalid Coupon',
|
||||
'no_assigned_tasks' => 'No billable tasks for this project',
|
||||
'authorization_failure' => 'Insufficient permissions to perform this action',
|
||||
'authorization_sms_failure' => 'Please verify your account to send emails.',
|
||||
'white_label_body' => 'Thank you for purchasing a white label license. <br><br> Your license key is: <br><br> :license_key',
|
||||
'change_plan_description' => 'Actualitzeu el vostre pla.',
|
||||
'add_company_logo' => 'Afegiu logo',
|
||||
'add_stripe' => 'Afegiu Stripe',
|
||||
'invalid_coupon' => 'Cupó invàlid',
|
||||
'no_assigned_tasks' => 'No hi ha cap tasca cobrable a aquest projecte',
|
||||
'authorization_failure' => 'Permisos insuficients per a realitzar aquesta acció',
|
||||
'authorization_sms_failure' => 'Verifiqueu el vostre compte per a poder enviar missatges de correu.',
|
||||
'white_label_body' => 'Gràcies per comprar una llicència de marca blanca. <br><br>La vostra clau de llicència és: <br><br>:license_key',
|
||||
'payment_type_Klarna' => 'Klarna',
|
||||
'payment_type_Interac E Transfer' => 'Interac E Transfer',
|
||||
'payment_type_Interac E Transfer' => 'Transferència Interac E',
|
||||
'xinvoice_payable' => 'Payable within :payeddue days net until :paydate',
|
||||
'xinvoice_no_buyers_reference' => "No buyer's reference given",
|
||||
'xinvoice_online_payment' => 'The invoice needs to be payed online via the provided link',
|
||||
'xinvoice_online_payment' => 'The invoice needs to be paid online via the provided link',
|
||||
'pre_payment' => 'Pre Payment',
|
||||
'number_of_payments' => 'Number of payments',
|
||||
'number_of_payments_helper' => 'The number of times this payment will be made',
|
||||
@ -4984,11 +4990,6 @@ $LANG = array(
|
||||
'notification_payment_emailed' => 'Payment :payment was emailed to :client',
|
||||
'notification_payment_emailed_subject' => 'Payment :payment was emailed',
|
||||
'record_not_found' => 'Record not found',
|
||||
'product_tax_exempt' => 'Product Tax Exempt',
|
||||
'product_type_physical' => 'Physical Goods',
|
||||
'product_type_digital' => 'Digital Goods',
|
||||
'product_type_service' => 'Services',
|
||||
'product_type_freight' => 'Shipping',
|
||||
'minimum_payment_amount' => 'Minimum Payment Amount',
|
||||
'client_initiated_payments' => 'Client Initiated Payments',
|
||||
'client_initiated_payments_help' => 'Support making a payment in the client portal without an invoice',
|
||||
@ -5057,6 +5058,34 @@ $LANG = array(
|
||||
'here' => 'here',
|
||||
'industry_Restaurant & Catering' => 'Restaurant & Catering',
|
||||
'show_credits_table' => 'Show Credits Table',
|
||||
'manual_payment' => 'Payment Manual',
|
||||
'tax_summary_report' => 'Tax Summary Report',
|
||||
'tax_category' => 'Tax Category',
|
||||
'physical_goods' => 'Physical Goods',
|
||||
'digital_products' => 'Digital Products',
|
||||
'services' => 'Services',
|
||||
'shipping' => 'Shipping',
|
||||
'tax_exempt' => 'Tax Exempt',
|
||||
'late_fee_added_locked_invoice' => 'Late fee for invoice :invoice added on :date',
|
||||
'lang_Khmer' => 'Khmer',
|
||||
'routing_id' => 'Routing ID',
|
||||
'enable_e_invoice' => 'Enable E-Invoice',
|
||||
'e_invoice_type' => 'E-Invoice Type',
|
||||
'reduced_tax' => 'Reduced Tax',
|
||||
'override_tax' => 'Override Tax',
|
||||
'zero_rated' => 'Zero Rated',
|
||||
'reverse_tax' => 'Reverse Tax',
|
||||
'updated_tax_category' => 'Successfully updated the tax category',
|
||||
'updated_tax_categories' => 'Successfully updated the tax categories',
|
||||
'set_tax_category' => 'Set Tax Category',
|
||||
'payment_manual' => 'Payment Manual',
|
||||
'expense_payment_type' => 'Expense Payment Type',
|
||||
'payment_type_Cash App' => 'Cash App',
|
||||
'rename' => 'Rename',
|
||||
'renamed_document' => 'Successfully renamed document',
|
||||
'e_invoice' => 'E-Invoice',
|
||||
'light_dark_mode' => 'Light/Dark Mode',
|
||||
'activities' => 'Activities',
|
||||
);
|
||||
|
||||
|
||||
|
@ -5086,6 +5086,7 @@ $LANG = array(
|
||||
'e_invoice' => 'E-Invoice',
|
||||
'light_dark_mode' => 'Light/Dark Mode',
|
||||
'activities' => 'Activities',
|
||||
'recent_transactions' => "Here are your company's most recent transactions:",
|
||||
);
|
||||
|
||||
|
||||
|
@ -5072,6 +5072,12 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
|
||||
'set_tax_category' => 'Définir la catégorie de taxe',
|
||||
'payment_manual' => 'Paiement manuel',
|
||||
'expense_payment_type' => 'Type de paiement de dépense',
|
||||
'payment_type_Cash App' => 'Cash App',
|
||||
'rename' => 'Renommer',
|
||||
'renamed_document' => 'Le document a été renommé',
|
||||
'e_invoice' => 'Facture électronique',
|
||||
'light_dark_mode' => 'Mode clair/sombre',
|
||||
'activities' => 'Activités',
|
||||
);
|
||||
|
||||
|
||||
|
@ -394,4 +394,4 @@ Route::post('api/v1/yodlee/data_updates', [YodleeController::class, 'dataUpdates
|
||||
Route::post('api/v1/yodlee/refresh_updates', [YodleeController::class, 'refreshUpdatesWebhook'])->middleware('throttle:100,1');
|
||||
Route::post('api/v1/yodlee/balance', [YodleeController::class, 'balanceWebhook'])->middleware('throttle:100,1');
|
||||
|
||||
Route::fallback([BaseController::class, 'notFound']);
|
||||
Route::fallback([BaseController::class, 'notFound'])->middleware('throttle:404');
|
@ -158,4 +158,4 @@ Route::fallback(function () {
|
||||
|
||||
abort(404);
|
||||
|
||||
});
|
||||
})->middleware('throttle:404');
|
||||
|
@ -143,6 +143,8 @@ class CompanyTest extends TestCase
|
||||
|
||||
$company->settings = $settings;
|
||||
|
||||
nlog($company->toArray());
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'X-API-SECRET' => config('ninja.api_secret'),
|
||||
'X-API-TOKEN' => $this->token,
|
||||
|
@ -100,7 +100,7 @@ class SchedulerTest extends TestCase
|
||||
'clients' => [],
|
||||
'report_keys' => [],
|
||||
'client_id' => $this->client->hashed_id,
|
||||
'report_name' => 'product_sales_report',
|
||||
'report_name' => 'product_sales',
|
||||
|
||||
],
|
||||
];
|
||||
@ -147,7 +147,7 @@ class SchedulerTest extends TestCase
|
||||
'clients' => [$this->client->hashed_id],
|
||||
'report_keys' => [],
|
||||
'client_id' => null,
|
||||
'report_name' => 'product_sales_report',
|
||||
'report_name' => 'product_sales',
|
||||
],
|
||||
];
|
||||
|
||||
@ -193,7 +193,7 @@ class SchedulerTest extends TestCase
|
||||
'clients' => [],
|
||||
'report_keys' => [],
|
||||
'client_id' => null,
|
||||
'report_name' => 'product_sales_report',
|
||||
'report_name' => 'product_sales',
|
||||
],
|
||||
];
|
||||
|
||||
@ -234,7 +234,7 @@ class SchedulerTest extends TestCase
|
||||
'parameters' => [
|
||||
'date_range' => EmailStatement::LAST_MONTH,
|
||||
'clients' => [],
|
||||
'report_name' => 'product_sales_report',
|
||||
'report_name' => 'product_sales',
|
||||
],
|
||||
];
|
||||
|
||||
|
@ -50,7 +50,7 @@ class ChartCurrencyTest extends TestCase
|
||||
|
||||
$this->assertDatabaseHas('invoices', ['number' => 'db_record']);
|
||||
|
||||
$cs = new ChartService($this->company);
|
||||
$cs = new ChartService($this->company, $this->user, true);
|
||||
// nlog($cs->getRevenueQuery(now()->subDays(20)->format('Y-m-d'), now()->addDays(100)->format('Y-m-d')));
|
||||
|
||||
$data = [
|
||||
@ -86,7 +86,7 @@ class ChartCurrencyTest extends TestCase
|
||||
'settings' => $settings,
|
||||
]);
|
||||
|
||||
$cs = new ChartService($this->company);
|
||||
$cs = new ChartService($this->company, $this->user, true);
|
||||
|
||||
$this->assertTrue(is_array($cs->getCurrencyCodes()));
|
||||
|
||||
@ -131,7 +131,7 @@ class ChartCurrencyTest extends TestCase
|
||||
],
|
||||
];
|
||||
|
||||
$cs = new ChartService($this->company);
|
||||
$cs = new ChartService($this->company, $this->user, true);
|
||||
|
||||
// nlog($cs->totals(now()->subYears(10), now()));
|
||||
|
||||
|
@ -70,6 +70,8 @@ class EuTaxTest extends TestCase
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'state' => 'CA',
|
||||
'postal_code' => '90210',
|
||||
'shipping_country_id' => 840,
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
@ -120,6 +122,307 @@ class EuTaxTest extends TestCase
|
||||
|
||||
}
|
||||
|
||||
public function testEuToBrazilTaxCalculations()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '276'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'DE';
|
||||
$tax_data->regions->EU->has_sales_above_threshold = false;
|
||||
$tax_data->regions->EU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 76,
|
||||
'shipping_country_id' => 76,
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testEuToAuTaxCalculationExemptProduct()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '276'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'DE';
|
||||
$tax_data->regions->EU->has_sales_above_threshold = false;
|
||||
$tax_data->regions->EU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'shipping_country_id' => 36,
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_EXEMPT,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testEuToAuTaxCalculationExemptClient()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '276'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'DE';
|
||||
$tax_data->regions->EU->has_sales_above_threshold = false;
|
||||
$tax_data->regions->EU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'shipping_country_id' => 36,
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => true,
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testEuToAuTaxCalculation()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '276'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'DE';
|
||||
$tax_data->regions->EU->has_sales_above_threshold = false;
|
||||
$tax_data->regions->EU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'shipping_country_id' => 36,
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
// 'tax_data' => new Response([
|
||||
// 'geoState' => 'CA',
|
||||
// 'taxSales' => 0.07,
|
||||
// ]),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(110, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testInvoiceTaxCalcDetoBeNoVat()
|
||||
{
|
||||
@ -444,6 +747,8 @@ class EuTaxTest extends TestCase
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'shipping_country_id' => 840,
|
||||
'state' => 'CA',
|
||||
'postal_code' => '90210',
|
||||
'has_valid_vat_number' => false,
|
||||
]);
|
||||
|
||||
@ -750,6 +1055,8 @@ class EuTaxTest extends TestCase
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'state' => 'CA',
|
||||
'postal_code' => '90210',
|
||||
'shipping_country_id' => 840,
|
||||
'has_valid_vat_number' => true,
|
||||
'is_tax_exempt' => true,
|
||||
|
@ -13,6 +13,7 @@ namespace Tests\Unit\Tax;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Models\Client;
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use Tests\MockAccountData;
|
||||
@ -20,6 +21,7 @@ use App\DataMapper\InvoiceItem;
|
||||
use App\DataMapper\Tax\TaxData;
|
||||
use App\Factory\InvoiceFactory;
|
||||
use App\DataMapper\Tax\TaxModel;
|
||||
use App\DataMapper\CompanySettings;
|
||||
use App\DataMapper\Tax\ZipTax\Response;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
@ -92,26 +94,32 @@ class SumTaxTest extends TestCase
|
||||
public function testCalcInvoiceNoTax()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = true;
|
||||
|
||||
$this->company->calculate_taxes = false;
|
||||
$this->company->tax_data = $tax_data;
|
||||
$this->company->save();
|
||||
|
||||
$tax_data = new TaxData($this->response);
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => false,
|
||||
'origin_tax_data' => new Response($this->resp),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'tax_data' => $tax_data,
|
||||
'state' => 'CA',
|
||||
'postal_code' => '90210',
|
||||
'tax_data' => new Response($this->resp),
|
||||
]);
|
||||
|
||||
$invoice = InvoiceFactory::create($this->company->id, $this->user->id);
|
||||
$invoice = InvoiceFactory::create($company->id, $this->user->id);
|
||||
$invoice->client_id = $client->id;
|
||||
$invoice->uses_inclusive_taxes = false;
|
||||
|
||||
@ -143,35 +151,39 @@ class SumTaxTest extends TestCase
|
||||
public function testCalcInvoiceTax()
|
||||
{
|
||||
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840';
|
||||
$settings->currency_id = '1';
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = true;
|
||||
|
||||
$this->company->calculate_taxes = true;
|
||||
$this->company->tax_data = $tax_data;
|
||||
$this->company->save();
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->resp),
|
||||
]);
|
||||
|
||||
$tax_data = new TaxData($this->response);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $this->company->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'tax_data' => $tax_data,
|
||||
'postal_code' => '90210',
|
||||
'state' => 'CA',
|
||||
'tax_data' => new Response($this->resp),
|
||||
]);
|
||||
|
||||
$invoice = InvoiceFactory::create($this->company->id, $this->user->id);
|
||||
$invoice = InvoiceFactory::create($company->id, $this->user->id);
|
||||
$invoice->client_id = $client->id;
|
||||
$invoice->uses_inclusive_taxes = false;
|
||||
|
||||
$line_items = [];
|
||||
|
||||
$invoice->tax_data = $tax_data;
|
||||
|
||||
|
||||
$line_item = new InvoiceItem;
|
||||
$line_item->quantity = 1;
|
||||
$line_item->cost = 10;
|
||||
@ -187,7 +199,6 @@ $invoice->tax_data = $tax_data;
|
||||
|
||||
$line_items = $invoice->line_items;
|
||||
|
||||
|
||||
$this->assertEquals(10.88, $invoice->amount);
|
||||
$this->assertEquals("CA Sales Tax", $line_items[0]->tax_name1);
|
||||
$this->assertEquals(8.75, $line_items[0]->tax_rate1);
|
||||
|
75
tests/Unit/Tax/TaxConfigTest.php
Normal file
75
tests/Unit/Tax/TaxConfigTest.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Tests\Unit\Tax;
|
||||
|
||||
use App\DataProviders\USStates;
|
||||
use Tests\TestCase;
|
||||
use App\Models\Client;
|
||||
use Tests\MockAccountData;
|
||||
use App\Services\Tax\Providers\TaxProvider;
|
||||
use Illuminate\Routing\Middleware\ThrottleRequests;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
||||
/**
|
||||
* @test App\Services\Tax\Providers\EuTax
|
||||
*/
|
||||
class TaxConfigTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp() :void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutMiddleware(
|
||||
ThrottleRequests::class
|
||||
);
|
||||
|
||||
$this->withoutExceptionHandling();
|
||||
|
||||
$this->makeTestData();
|
||||
|
||||
if(!config('services.tax.zip_tax.key'))
|
||||
$this->markTestSkipped('No API keys to test with.');
|
||||
}
|
||||
|
||||
public TaxProvider $tp;
|
||||
|
||||
private function bootApi(Client $client)
|
||||
{
|
||||
$this->tp = new TaxProvider($this->company, $client);
|
||||
}
|
||||
|
||||
public function testStateResolution()
|
||||
{
|
||||
//infer state from zip
|
||||
$client = Client::factory()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'user_id' => $this->user->id,
|
||||
'address1' => '400 Evelyn Pl',
|
||||
'city' => 'Beverley Hills',
|
||||
'state' => '',
|
||||
'postal_code' => '',
|
||||
'country_id' => 840,
|
||||
]);
|
||||
|
||||
|
||||
// $this->assertEquals('CA', USStates::getState('90210'));
|
||||
|
||||
$this->bootApi($client);
|
||||
|
||||
$this->tp->updateClientTaxData();
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -98,6 +98,8 @@ class UsTaxTest extends TestCase
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
@ -147,6 +149,591 @@ class UsTaxTest extends TestCase
|
||||
return $invoice;
|
||||
}
|
||||
|
||||
public function testTaxAuNoExemption()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 36,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'NSW'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(110, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testTaxAuClientExemption()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 36,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => true,
|
||||
'state' => 'NSW'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testTaxAuProductExemption()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 36,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'NSW'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_EXEMPT,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testTaxAuProductOverride()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
$tax_data->regions->AU->has_sales_above_threshold = true;
|
||||
$tax_data->regions->AU->tax_all_subregions = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 36,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 36,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'NSW'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => 'OVERRIDE',
|
||||
'tax_rate1' => 20,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_OVERRIDE_TAX,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(120, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testInterstateFreightNoTaxWithProductTax()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = true;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 840,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'GA'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_SHIPPING,
|
||||
],
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(208.75, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testInterstateFreightProductNoTax()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 840,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'GA'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_SHIPPING,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function testInterstateServiceProductNoTax()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 840,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'GA'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '2',
|
||||
'tax_id' => Product::PRODUCT_TYPE_SERVICE,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testInterstateWithNoTax()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = false;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'postal_code' => '30002',
|
||||
'shipping_country_id' => 840,
|
||||
'shipping_postal_code' => '30002',
|
||||
'shipping_state' => '30002',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => false,
|
||||
'state' => 'GA'
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
public function testSameSubregionAndExemptProduct()
|
||||
{
|
||||
|
||||
@ -166,6 +753,7 @@ class UsTaxTest extends TestCase
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
@ -218,6 +806,78 @@ class UsTaxTest extends TestCase
|
||||
|
||||
}
|
||||
|
||||
public function testSameSubregionAndExemptClient()
|
||||
{
|
||||
|
||||
$settings = CompanySettings::defaults();
|
||||
$settings->country_id = '840'; // germany
|
||||
|
||||
$tax_data = new TaxModel();
|
||||
$tax_data->seller_subregion = 'CA';
|
||||
$tax_data->regions->US->has_sales_above_threshold = true;
|
||||
$tax_data->regions->US->tax_all_subregions = true;
|
||||
$tax_data->regions->EU->has_sales_above_threshold = true;
|
||||
$tax_data->regions->EU->tax_all_subregions = true;
|
||||
$tax_data->regions->EU->subregions->DE->tax_rate = 21;
|
||||
|
||||
$company = Company::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
'company_id' => $company->id,
|
||||
'country_id' => 840,
|
||||
'postal_code' => '90210',
|
||||
'shipping_country_id' => 840,
|
||||
'shipping_postal_code' => '90210',
|
||||
'has_valid_vat_number' => false,
|
||||
'is_tax_exempt' => true,
|
||||
]);
|
||||
|
||||
$invoice = Invoice::factory()->create([
|
||||
'company_id' => $company->id,
|
||||
'client_id' => $client->id,
|
||||
'status_id' => 1,
|
||||
'user_id' => $this->user->id,
|
||||
'uses_inclusive_taxes' => false,
|
||||
'discount' => 0,
|
||||
'line_items' => [
|
||||
[
|
||||
'product_key' => 'Test',
|
||||
'notes' => 'Test',
|
||||
'cost' => 100,
|
||||
'quantity' => 1,
|
||||
'tax_name1' => '',
|
||||
'tax_rate1' => 0,
|
||||
'tax_name2' => '',
|
||||
'tax_rate2' => 0,
|
||||
'tax_name3' => '',
|
||||
'tax_rate3' => 0,
|
||||
'type_id' => '1',
|
||||
'tax_id' => Product::PRODUCT_TYPE_PHYSICAL,
|
||||
],
|
||||
],
|
||||
'tax_rate1' => 0,
|
||||
'tax_rate2' => 0,
|
||||
'tax_rate3' => 0,
|
||||
'tax_name1' => '',
|
||||
'tax_name2' => '',
|
||||
'tax_name3' => '',
|
||||
'tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$invoice = $invoice->calc()->getInvoice()->service()->markSent()->save();
|
||||
|
||||
$this->assertEquals(100, $invoice->amount);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public function testForeignTaxesEnabledWithExemptProduct()
|
||||
{
|
||||
$settings = CompanySettings::defaults();
|
||||
@ -236,6 +896,7 @@ class UsTaxTest extends TestCase
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
@ -305,6 +966,7 @@ class UsTaxTest extends TestCase
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
@ -373,6 +1035,7 @@ class UsTaxTest extends TestCase
|
||||
'settings' => $settings,
|
||||
'tax_data' => $tax_data,
|
||||
'calculate_taxes' => true,
|
||||
'origin_tax_data' => new Response($this->mock_response),
|
||||
]);
|
||||
|
||||
$client = Client::factory()->create([
|
||||
|
Loading…
x
Reference in New Issue
Block a user