diff --git a/app/Casts/EncryptedCast.php b/app/Casts/EncryptedCast.php new file mode 100644 index 000000000000..e13c69625d32 --- /dev/null +++ b/app/Casts/EncryptedCast.php @@ -0,0 +1,27 @@ + 1 ? decrypt($value) : null; + } + + public function set($model, string $key, $value, array $attributes) + { + return [$key => ! is_null($value) ? encrypt($value) : null]; + } +} diff --git a/app/DataMapper/Tax/BaseRule.php b/app/DataMapper/Tax/BaseRule.php index 6f664dd8148f..1cf893d5dded 100644 --- a/app/DataMapper/Tax/BaseRule.php +++ b/app/DataMapper/Tax/BaseRule.php @@ -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; @@ -130,70 +127,140 @@ 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($this->invoice instanceof Invoice) { - $this->invoice->tax_data = $tax_data; - $this->invoice->saveQuietly(); } - + + /** 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) { + + $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))); + } + } diff --git a/app/DataMapper/Tax/DE/Rule.php b/app/DataMapper/Tax/DE/Rule.php index 2118adf02e98..7cb58bc938be 100644 --- a/app/DataMapper/Tax/DE/Rule.php +++ b/app/DataMapper/Tax/DE/Rule.php @@ -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; } diff --git a/app/DataMapper/Tax/RuleInterface.php b/app/DataMapper/Tax/RuleInterface.php index 6cd933567645..e4ff2c0e2da9 100644 --- a/app/DataMapper/Tax/RuleInterface.php +++ b/app/DataMapper/Tax/RuleInterface.php @@ -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(); } \ No newline at end of file diff --git a/app/DataMapper/Tax/US/Rule.php b/app/DataMapper/Tax/US/Rule.php index 854a689cae2a..4a4527057b9f 100644 --- a/app/DataMapper/Tax/US/Rule.php +++ b/app/DataMapper/Tax/US/Rule.php @@ -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,23 +157,13 @@ 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; - - } + $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; return $this; } @@ -165,22 +171,43 @@ class Rule extends BaseRule implements RuleInterface $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; + + } + /** * Calculates the tax rate for a reduced tax product * * @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; + } + /** * Calculates the tax rates to be applied * @@ -190,4 +217,5 @@ class Rule extends BaseRule implements RuleInterface { return $this; } + } diff --git a/app/DataProviders/USStates.php b/app/DataProviders/USStates.php index d973bfe5f4e7..3f07c4ecf427 100644 --- a/app/DataProviders/USStates.php +++ b/app/DataProviders/USStates.php @@ -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', diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index dd2d9b2e434e..3be7e878ceb8 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -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 + 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(); diff --git a/app/Http/Controllers/ChartController.php b/app/Http/Controllers/ChartController.php index 8800768fc81d..651ded493fbb 100644 --- a/app/Http/Controllers/ChartController.php +++ b/app/Http/Controllers/ChartController.php @@ -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); } diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 6b32a6bee2e4..a7900a3f173c 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -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); diff --git a/app/Http/Controllers/EmailController.php b/app/Http/Controllers/EmailController.php index 8cd59930b6d2..b7e692d3726d 100644 --- a/app/Http/Controllers/EmailController.php +++ b/app/Http/Controllers/EmailController.php @@ -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'); diff --git a/app/Http/Requests/Client/UpdateClientRequest.php b/app/Http/Requests/Client/UpdateClientRequest.php index 8b4059c9aac4..24cf207bad0f 100644 --- a/app/Http/Requests/Client/UpdateClientRequest.php +++ b/app/Http/Requests/Client/UpdateClientRequest.php @@ -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) { diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index f1c66b0a3a4f..4b72c48ee8bf 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -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); } diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index d7004836bd9c..d74dee599711 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -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 */ diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index f1e5f3b58082..f613c7881a55 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -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); + } } diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index d8bf21970622..48c52a3e3e0f 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -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); } } diff --git a/app/Import/Transformer/Zoho/ClientTransformer.php b/app/Import/Transformer/Zoho/ClientTransformer.php index b5014960c4d7..6eafc140e6d3 100644 --- a/app/Import/Transformer/Zoho/ClientTransformer.php +++ b/app/Import/Transformer/Zoho/ClientTransformer.php @@ -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; } } diff --git a/app/Import/Transformer/Zoho/InvoiceTransformer.php b/app/Import/Transformer/Zoho/InvoiceTransformer.php index 1ec99eca3520..45b72b39ce88 100644 --- a/app/Import/Transformer/Zoho/InvoiceTransformer.php +++ b/app/Import/Transformer/Zoho/InvoiceTransformer.php @@ -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; + + } } diff --git a/app/Jobs/Client/UpdateTaxData.php b/app/Jobs/Client/UpdateTaxData.php new file mode 100644 index 000000000000..1f640f9a0b34 --- /dev/null +++ b/app/Jobs/Client/UpdateTaxData.php @@ -0,0 +1,78 @@ +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()); + } + + + } +} \ No newline at end of file diff --git a/app/Jobs/Company/CreateCompany.php b/app/Jobs/Company/CreateCompany.php index 1c0328539a60..f288b1b9f52f 100644 --- a/app/Jobs/Company/CreateCompany.php +++ b/app/Jobs/Company/CreateCompany.php @@ -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; + + } + } diff --git a/app/Jobs/Entity/EmailEntity.php b/app/Jobs/Entity/EmailEntity.php index 82cbef192937..41473f1b8fd1 100644 --- a/app/Jobs/Entity/EmailEntity.php +++ b/app/Jobs/Entity/EmailEntity.php @@ -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() { diff --git a/app/Listeners/Invoice/InvoiceEmailedNotification.php b/app/Listeners/Invoice/InvoiceEmailedNotification.php index bf0859e2da25..1f37e7938dcb 100644 --- a/app/Listeners/Invoice/InvoiceEmailedNotification.php +++ b/app/Listeners/Invoice/InvoiceEmailedNotification.php @@ -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; diff --git a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php index 1cf801afec62..9b41a3b096b6 100644 --- a/app/Listeners/Invoice/InvoiceReminderEmailActivity.php +++ b/app/Listeners/Invoice/InvoiceReminderEmailActivity.php @@ -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); } diff --git a/app/Models/Company.php b/app/Models/Company.php index 2b0d05a12cbc..c0bb8f2df705 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -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, diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 54d37fb968ca..dfdf67c2691a 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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... diff --git a/app/Observers/ClientObserver.php b/app/Observers/ClientObserver.php index ed0a1e7c3310..814ee7f0c421 100644 --- a/app/Observers/ClientObserver.php +++ b/app/Observers/ClientObserver.php @@ -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) { @@ -53,8 +64,7 @@ class ClientObserver if ($client->is_deleted) { $event = Webhook::EVENT_DELETE_CLIENT; } - - + $subscriptions = Webhook::where('company_id', $client->company_id) ->where('event_id', $event) ->exists(); diff --git a/app/Observers/CompanyObserver.php b/app/Observers/CompanyObserver.php index 122518568224..93c2aeaf68c7 100644 --- a/app/Observers/CompanyObserver.php +++ b/app/Observers/CompanyObserver.php @@ -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()); + // } + } /** diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 05833ccecc63..749b52770903 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -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) { diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index bdac90e9c0b2..e5cbc4b895c4 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -777,7 +777,8 @@ class StripePaymentDriver extends BaseDriver ->where('token', $request->data['object']['payment_method']) ->first(); - $clientgateway->delete(); + if($clientgateway) + $clientgateway->delete(); return response()->json([], 200); } elseif ($request->data['object']['status'] == "pending") { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 44cd2b1b70a0..9bcdc1f4682a 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -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()); + } + }); + } /** diff --git a/app/Services/Chart/ChartQueries.php b/app/Services/Chart/ChartQueries.php index f4d0fb509d39..7f284736c58b 100644 --- a/app/Services/Chart/ChartQueries.php +++ b/app/Services/Chart/ChartQueries.php @@ -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 diff --git a/app/Services/Chart/ChartService.php b/app/Services/Chart/ChartService.php index 52673fe5403b..c9a8be50eda7 100644 --- a/app/Services/Chart/ChartService.php +++ b/app/Services/Chart/ChartService.php @@ -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'); diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 5a2bcce7c35b..79840dad0beb 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -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 ]); diff --git a/app/Services/Scheduler/EmailReport.php b/app/Services/Scheduler/EmailReport.php index 8ec4c020630e..ce76e54fad04 100644 --- a/app/Services/Scheduler/EmailReport.php +++ b/app/Services/Scheduler/EmailReport.php @@ -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)), diff --git a/app/Services/Tax/Providers/TaxProvider.php b/app/Services/Tax/Providers/TaxProvider.php index 4d71522cf617..ff92d455ed65 100644 --- a/app/Services/Tax/Providers/TaxProvider.php +++ b/app/Services/Tax/Providers/TaxProvider.php @@ -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->client->tax_data = $tax_data; - $this->company->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 diff --git a/app/Services/Tax/Providers/ZipTax.php b/app/Services/Tax/Providers/ZipTax.php index b6f90815db11..9f0e40b8b4de 100644 --- a/app/Services/Tax/Providers/ZipTax.php +++ b/app/Services/Tax/Providers/ZipTax.php @@ -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']); + + } } diff --git a/app/Services/Tax/TaxService.php b/app/Services/Tax/TaxService.php index 7213b023c5f1..89dd1ba7c31c 100644 --- a/app/Services/Tax/TaxService.php +++ b/app/Services/Tax/TaxService.php @@ -31,4 +31,9 @@ class TaxService return $this; } + + public function initTaxProvider() + { + + } } \ No newline at end of file diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index eff5e27b6be7..4152fa1988ff 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -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, ]; } diff --git a/app/Utils/Traits/CleanLineItems.php b/app/Utils/Traits/CleanLineItems.php index 921f66c32e1b..19938c0dce0b 100644 --- a/app/Utils/Traits/CleanLineItems.php +++ b/app/Utils/Traits/CleanLineItems.php @@ -66,7 +66,12 @@ trait CleanLineItems $item['tax_id'] = '1'; } elseif(array_key_exists('tax_id', $item) && $item['tax_id'] == '') { - $item['tax_id'] = '1'; + + if($item['type_id'] == '2') + $item['tax_id'] = '2'; + else + $item['tax_id'] = '1'; + } } diff --git a/config/ninja.php b/config/ninja.php index 297e6880d877..f61cf2828fd5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -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), - ] ]; diff --git a/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php new file mode 100644 index 000000000000..5f13774326e0 --- /dev/null +++ b/database/migrations/2023_05_15_103212_e_invoice_ssl_storage.php @@ -0,0 +1,34 @@ +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() + { + // + } +}; diff --git a/lang/ca/texts.php b/lang/ca/texts.php index 3918a8576834..7d268f0c3eec 100644 --- a/lang/ca/texts.php +++ b/lang/ca/texts.php @@ -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 impost', - 'enable_line_item_tax' => 'Activar especificar impost per línea', + 'enable_invoice_tax' => 'Activa especificar impost', + 'enable_line_item_tax' => 'Activa especificar impost per línea', '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' => '
Envieu automàticament als clients les mateixes factures setmanalment, bimensuals, mensuals, trimestrals o anuals.
-Utilitzeu: MONTH,: TRIMESTRE o: YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: MES-1
-', +Utilitzeu :MONTH, :QUARTER o :YEAR per a dates dinàmiques. Les funcions matemàtiques bàsiques també funcionen, per exemple: :MONTH-1
+Exemples de variables dinàmiques de factures:
+