Merge pull request #11571 from turbo124/v5-develop

v5.12.46
This commit is contained in:
David Bomba
2026-01-15 07:01:41 +11:00
committed by GitHub
32 changed files with 431 additions and 93 deletions

View File

@@ -1 +1 @@
5.12.45
5.12.46

View File

@@ -236,7 +236,7 @@ class PayPalBalanceAffecting
// $csv = Reader::createFromString($csvFile);
// $csv = Reader::fromString($csvFile);
// // $csvdelimiter = self::detectDelimiter($csvfile);
// $csv->setDelimiter(",");
// $stmt = new Statement();

View File

@@ -11,37 +11,42 @@
namespace App\Helpers\Cache;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Redis;
class Atomic
{
public static function set($key, $value = true, $ttl = 1): bool
public static function set(string $key, mixed $value = true, int $ttl = 1): bool
{
$new_ttl = now()->addSeconds($ttl);
try {
return Redis::connection('sentinel-cache')->set($key, $value, 'EX', $ttl, 'NX') ? true : false;
/** @var RedisFactory $redis */
$redis = app('redis');
$result = $redis->connection('sentinel-cache')->command('set', [$key, $value, 'EX', $ttl, 'NX']);
return (bool) $result;
} catch (\Throwable) {
return Cache::add($key, $value, $new_ttl) ? true : false;
}
}
public static function get($key)
public static function get(string $key): mixed
{
try {
return Redis::connection('sentinel-cache')->get($key);
/** @var RedisFactory $redis */
$redis = app('redis');
return $redis->connection('sentinel-cache')->command('get', [$key]);
} catch (\Throwable) {
return Cache::get($key);
}
}
public static function del($key)
public static function del(string $key): mixed
{
try {
return Redis::connection('sentinel-cache')->del($key);
/** @var RedisFactory $redis */
$redis = app('redis');
return $redis->connection('sentinel-cache')->command('del', [$key]);
} catch (\Throwable) {
return Cache::forget($key);
}

View File

@@ -112,11 +112,11 @@ class SwissQrGenerator
// Add payment reference
// This is what you will need to identify incoming payments.
if (stripos($this->invoice->number, "Live") === 0) {
if (stripos($this->invoice->number ?? '', "Live") === 0) {
// we're currently in preview status. Let's give a dummy reference for now
$invoice_number = "123456789";
} else {
$tempInvoiceNumber = $this->invoice->number;
$tempInvoiceNumber = $this->invoice->number ?? '';
$tempInvoiceNumber = preg_replace('/[^A-Za-z0-9]/', '', $tempInvoiceNumber);
// $tempInvoiceNumber = substr($tempInvoiceNumber, 1);

View File

@@ -22,8 +22,8 @@ use App\Jobs\Util\ApplePayDomain;
use Illuminate\Support\Facades\Cache;
use App\Factory\CompanyGatewayFactory;
use App\Filters\CompanyGatewayFilters;
use App\Repositories\CompanyRepository;
use Illuminate\Foundation\Bus\DispatchesJobs;
use App\Repositories\CompanyGatewayRepository;
use App\Transformers\CompanyGatewayTransformer;
use App\PaymentDrivers\Stripe\Jobs\StripeWebhook;
use App\PaymentDrivers\CheckoutCom\CheckoutSetupWebhook;
@@ -63,9 +63,9 @@ class CompanyGatewayController extends BaseController
/**
* CompanyGatewayController constructor.
* @param CompanyRepository $company_repo
* @param CompanyGatewayRepository $company_repo
*/
public function __construct(CompanyRepository $company_repo)
public function __construct(CompanyGatewayRepository $company_repo)
{
parent::__construct();
@@ -210,10 +210,14 @@ class CompanyGatewayController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
$company = $user->company();
$company_gateway = CompanyGatewayFactory::create($user->company()->id, $user->id);
$company_gateway->fill($request->all());
$company_gateway->save();
$this->company_repo->addGatewayToCompanyGatewayIds($company_gateway);
/*Always ensure at least one fees and limits object is set per gateway*/
$gateway_types = $company_gateway->driver(new Client())->getAvailableMethods();

View File

@@ -446,7 +446,6 @@ class ImportController extends Controller
$csv = Reader::fromString($csvfile);
// $csv = Reader::createFromString($csvfile);
$csvdelimiter = self::detectDelimiter($csvfile);
$csv->setDelimiter($csvdelimiter);
$stmt = new Statement();
@@ -456,7 +455,7 @@ class ImportController extends Controller
$headers = $data[0];
// Remove Invoice Ninja headers
if (count($headers) && count($data) > 4) {
if (is_array($headers) && count($headers) > 0 && count($data) > 4) {
$firstCell = $headers[0];
if (strstr($firstCell, (string) config('ninja.app_name'))) {

View File

@@ -161,6 +161,10 @@ class StoreTaskRequest extends Request
}
if(isset($input['description']) && is_string($input['description'])) {
$input['description'] = str_ireplace(['</sc', 'file:/', 'iframe', '<embed', '&lt;embed', '&lt;object', '<object', '127.0.0.1', 'localhost', '<?xml encoding="UTF-8">', '/etc/'], "", $input['description']);
}
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->where('id', $input['project_id'])->company()->first();

View File

@@ -136,6 +136,10 @@ class UpdateTaskRequest extends Request
$input['status_id'] = $this->decodePrimaryKey($input['status_id']);
}
if(isset($input['description']) && is_string($input['description'])) {
$input['description'] = str_ireplace(['</sc', 'file:/', 'iframe', '<embed', '&lt;embed', '&lt;object', '<object', '127.0.0.1', 'localhost', '<?xml encoding="UTF-8">', '/etc/'], "", $input['description']);
}
if (isset($input['documents'])) {
unset($input['documents']);
}

View File

@@ -12,33 +12,35 @@
namespace App\Import\Providers;
use App\Models\User;
use App\Utils\Ninja;
use App\Models\Quote;
use League\Csv\Reader;
use App\Models\Company;
use App\Models\Invoice;
use League\Csv\Statement;
use App\Factory\TaskFactory;
use App\Factory\QuoteFactory;
use App\Factory\ClientFactory;
use Illuminate\Support\Carbon;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\QuoteFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\TaskFactory;
use App\Http\Requests\Quote\StoreQuoteRequest;
use App\Import\ImportException;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Import\CsvImportCompleted;
use App\Models\Company;
use App\Models\Invoice;
use App\Models\Quote;
use App\Models\User;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Repositories\QuoteRepository;
use App\Repositories\RecurringInvoiceRepository;
use App\Repositories\TaskRepository;
use App\Utils\Traits\CleanLineItems;
use Illuminate\Support\Carbon;
use App\Repositories\QuoteRepository;
use Illuminate\Support\Facades\Cache;
use App\Repositories\ClientRepository;
use App\Mail\Import\CsvImportCompleted;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Factory\RecurringInvoiceFactory;
use Illuminate\Support\Facades\Validator;
use League\Csv\Reader;
use League\Csv\Statement;
use App\Http\Requests\Quote\StoreQuoteRequest;
use App\Repositories\RecurringInvoiceRepository;
use App\Notifications\Ninja\GenericNinjaAdminNotification;
class BaseImport
{
@@ -70,6 +72,8 @@ class BaseImport
public array $entity_count = [];
public bool $store_import_for_research = false;
public function __construct(array $request, Company $company)
{
$this->company = $company;
@@ -107,7 +111,7 @@ class BaseImport
$csv = base64_decode($base64_encoded_csv);
// $csv = mb_convert_encoding($csv, 'UTF-8', 'UTF-8');
$csv = Reader::createFromString($csv);
$csv = Reader::fromString($csv);
$csvdelimiter = self::detectDelimiter($csv);
$csv->setDelimiter($csvdelimiter);
@@ -119,7 +123,8 @@ class BaseImport
// Remove Invoice Ninja headers
if (
count($headers) &&
is_array($headers) &&
count($headers) > 0 &&
count($data) > 4 &&
$this->import_type === 'csv'
) {
@@ -320,7 +325,8 @@ class BaseImport
$entity->saveQuietly();
$count++;
}
} catch (\Exception $ex) {
}
catch (\Exception $ex) {
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
\DB::connection(config('database.default'))->rollBack();
}
@@ -339,6 +345,20 @@ class BaseImport
nlog("Ingest {$ex->getMessage()}");
nlog($record);
$this->store_import_for_research = true;
}
catch(\Throwable $ex){
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
\DB::connection(config('database.default'))->rollBack();
}
nlog("Throwable:: Ingest {$ex->getMessage()}");
nlog($record);
$this->store_import_for_research = true;
}
}
@@ -945,6 +965,39 @@ class BaseImport
$nmo->to_user = $this->company->owner();
NinjaMailerJob::dispatch($nmo, true);
/** Debug for import failures */
if (Ninja::isHosted() && $this->store_import_for_research) {
$content = [
'company_key - '. $this->company->company_key,
'class_name - ' . class_basename($this),
'hash - ' => $this->hash,
];
$potential_imports = [
'client',
'product',
'invoice',
'payment',
'vendor',
'expense',
'quote',
'bank_transaction',
'task',
'recurring_invoice',
];
foreach ($potential_imports as $import) {
if(Cache::has($this->hash.'-'.$import)) {
Cache::put($this->hash.'-'.$import, Cache::get($this->hash.'-'.$import), 60*60*24*2);
}
}
$this->company->notification(new GenericNinjaAdminNotification($content))->ninja();
}
}
public function preTransform(array $data, $entity_type)

View File

@@ -382,7 +382,6 @@ class Csv extends BaseImport implements ImportInterface
$this->entity_count['tasks'] = $task_count;
}
public function transform(array $data)

View File

@@ -166,7 +166,7 @@ class ProcessPostmarkWebhook implements ShouldQueue
private function processOpen()
{
$this->invitation->opened_date = now();
$this->invitation->opened_date = now()->setTimezone($this->invitation->company->timezone()->name);
$this->invitation->saveQuietly();
$data = array_merge($this->request, ['history' => $this->fetchMessage()]);

View File

@@ -279,7 +279,7 @@ class BaseModel extends Model
public function numberFormatter()
{
$number = strlen($this->number) >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5);
$number = strlen($this->number ?? '') >= 1 ? $this->translate_entity() . "_" . $this->number : class_basename($this) . "_" . Str::random(5);
$formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $number);

View File

@@ -30,7 +30,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property bool $is_deleted
* @property string $config
* @property object $settings
* @property mixed $fees_and_limits
* @property array|object|mixed $fees_and_limits
* @property string|null $custom_value1
* @property string|null $custom_value2
* @property string|null $custom_value3

View File

@@ -38,7 +38,7 @@ use App\Utils\Number;
* App\Models\Invoice
*
* @property int $id
* @property object|null $e_invoice
* @property object|array|null $e_invoice
* @property int $client_id
* @property int $user_id
* @property int|null $location_id

View File

@@ -83,7 +83,8 @@ class PaymentIntentProcessingWebhook implements ShouldQueue
/** @var \App\Models\ClientGatewayToken $cgt **/
$cgt = ClientGatewayToken::where('token', $transaction['payment_method'])->first();
if ($cgt && $cgt->meta?->state == 'unauthorized') {
if ($cgt && isset($cgt->meta)) {
// if ($cgt && $cgt->meta?->state == 'unauthorized') {
$meta = $cgt->meta;
$meta->state = 'authorized';
$cgt->meta = $meta;

View File

@@ -0,0 +1,94 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Repositories;
use App\Utils\Ninja;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Repositories\BaseRepository;
/**
* CompanyGatewayRepository.
*/
class CompanyGatewayRepository extends BaseRepository
{
public function __construct()
{
}
public function archive($company_gateway): CompanyGateway
{
parent::archive($company_gateway);
$this->removeGatewayFromCompanyGatewayIds($company_gateway);
return $company_gateway;
}
public function delete($company_gateway): CompanyGateway
{
parent::delete($company_gateway);
$this->removeGatewayFromCompanyGatewayIds($company_gateway);
return $company_gateway;
}
public function restore($company_gateway): CompanyGateway
{
parent::restore($company_gateway);
$this->addGatewayToCompanyGatewayIds($company_gateway);
return $company_gateway;
}
public function addGatewayToCompanyGatewayIds(CompanyGateway $company_gateway)
{
$company_gateway_ids = $company_gateway->company->getSetting('company_gateway_ids');
if(strlen($company_gateway_ids ?? '') > 2){
$transformed_ids = collect($this->transformKeys(explode(',', $company_gateway_ids)))
->push($company_gateway->hashed_id)
->implode(",");
$company = $company_gateway->company;
$settings = $company->settings;
$settings->company_gateway_ids = $transformed_ids;
$company->settings = $settings;
$company->save();
}
}
public function removeGatewayFromCompanyGatewayIds(CompanyGateway $company_gateway)
{
$company_gateway_ids = $company_gateway->company->getSetting('company_gateway_ids');
if(strpos($company_gateway_ids, $company_gateway->hashed_id) !== false){
$transformed_ids = collect($this->transformKeys(explode(',', $company_gateway_ids)))
->filter(function ($id) use ($company_gateway){
return $id !== $company_gateway->hashed_id;
})
->implode(",");
$company = $company_gateway->company;
$settings = $company->settings;
$settings->company_gateway_ids = $transformed_ids;
$company->settings = $settings;
$company->save();
}
}
}

View File

@@ -219,6 +219,9 @@ class StorecoveRouter
$country = 'BE';
$identifier = 'BE:VAT';
}
elseif($country == 'GLN'){
return 'routing_id';
}
$rules = $this->routing_rules[$country];

View File

@@ -189,13 +189,12 @@ class StorecoveExpense
$tax_totals[] = (array)$tdf;
}
$totals = collect($tax_totals);
$party = $storecove_invoice->getAccountingSupplierParty()->getParty();
$pis = $storecove_invoice->getAccountingSupplierParty()->getPublicIdentifiers();
$vat_number = '';
$id_number = '';
$routing_id = '';
foreach ($pis as $pi) {
if ($ident = $this->storecove->router->resolveIdentifierTypeByValue($pi->getScheme())) {
@@ -203,6 +202,8 @@ class StorecoveExpense
$vat_number = $pi->getId();
} elseif ($ident == 'id_number') {
$id_number = $pi->getId();
} elseif ($ident == 'routing_id') {
$routing_id = $pi->getId();
}
}
}
@@ -271,6 +272,7 @@ class StorecoveExpense
'currency_id' => $currency,
'id_number' => $id_number,
'vat_number' => $vat_number,
'routing_id' => $routing_id,
'address1' => $party->getAddress()->getStreet1() ?? '',
'address2' => $party->getAddress()->getStreet2() ?? '',
'city' => $party->getAddress()->getCity() ?? '',

View File

@@ -65,7 +65,7 @@ class Peppol extends AbstractService
*
*/
private ?string $override_vat_number;
private string $override_vat_number = '';
/** @var array $InvoiceTypeCodes */
private array $InvoiceTypeCodes = [
@@ -657,7 +657,8 @@ class Peppol extends AbstractService
$tax_type = 'S';
break;
case Product::PRODUCT_TYPE_REDUCED_TAX:
$tax_type = 'AA';
// $tax_type = 'AA';
$tax_type = 'S'; //2026-01-14 - using AA breaks PEPPOL VALIDATION!!
break;
case Product::PRODUCT_TYPE_EXEMPT:
$tax_type = 'E';
@@ -1032,18 +1033,18 @@ class Peppol extends AbstractService
$party->PartyName[] = $party_name;
if (strlen($this->company->settings->vat_number ?? '') > 1) {
$pi = new PartyIdentification();
$vatID = new ID();
$vatID->schemeID = $this->resolveScheme();
$vatID->value = $this->override_vat_number ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number
$vatID->value = strlen($this->override_vat_number ?? '') > 1 ? $this->override_vat_number : preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number
$pi->ID = $vatID;
$party->PartyIdentification[] = $pi;
$pts = new \InvoiceNinja\EInvoice\Models\Peppol\PartyTaxSchemeType\PartyTaxScheme();
$companyID = new \InvoiceNinja\EInvoice\Models\Peppol\IdentifierType\CompanyID();
$companyID->value = $this->override_vat_number ?? preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number);
$companyID->value = strlen($this->override_vat_number ?? '') > 1 ? $this->override_vat_number : preg_replace("/[^a-zA-Z0-9]/", "", $this->invoice->company->settings->vat_number); //todo if we are cross border - switch to the supplier local vat number
$pts->CompanyID = $companyID;
$ts = new TaxScheme();
@@ -1290,7 +1291,6 @@ class Peppol extends AbstractService
///////////////// Helper Methods /////////////////////////
/**
* setInvoiceDefaults
*
@@ -1498,9 +1498,9 @@ class Peppol extends AbstractService
$category_id->value = $this->getTaxType($grouped_tax['tax_id']); // Standard rate
// Temp fix for reduced tax rate categorization.
if($grouped_tax['tax_rate'] < 15 && $grouped_tax['tax_rate'] >= 0) {
$category_id->value = 'AA';
}
// if($grouped_tax['tax_rate'] < 15 && $grouped_tax['tax_rate'] >= 0) {
// $category_id->value = 'AA';
// }
$tax_category->ID = $category_id;

View File

@@ -33,9 +33,12 @@ use App\Events\Invoice\InvoiceWasPaid;
use App\Repositories\CreditRepository;
use App\Repositories\PaymentRepository;
use App\Events\Payment\PaymentWasCreated;
use App\Utils\Traits\MakesHash;
class AutoBillInvoice extends AbstractService
{
use MakesHash;
private Client $client;
private array $used_credit = [];
@@ -45,9 +48,7 @@ class AutoBillInvoice extends AbstractService
public function __construct(private Invoice $invoice, protected string $db)
{
$this->client = $this->invoice->client;
}
public function run()
@@ -55,7 +56,6 @@ class AutoBillInvoice extends AbstractService
MultiDB::setDb($this->db);
/* @var \App\Modesl\Client $client */
$is_partial = false;
/* Is the invoice payable? */
@@ -444,14 +444,32 @@ class AutoBillInvoice extends AbstractService
*/
public function getGateway($amount)
{
$company_gateway_ids = $this->client->getSetting('company_gateway_ids');
$transformed_ids = false;
//gateways are disabled!
if($company_gateway_ids == "0") {
return false;
}
elseif(strlen($company_gateway_ids ?? '') > 2){
// If the client has a special gateway configuration, we need to ensure we only use the ones that are enabled!
$transformed_ids = $this->transformKeys(explode(',', $company_gateway_ids));
}
//get all client gateway tokens and set the is_default one to the first record
$gateway_tokens = \App\Models\ClientGatewayToken::query()
->where('client_id', $this->client->id)
->where('is_deleted', 0)
->whereHas('gateway', function ($query) {
->whereHas('gateway', function ($query) use ($transformed_ids) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})->orderBy('is_default', 'DESC')
->where('deleted_at', null)
->when($transformed_ids, function ($q) use ($transformed_ids) {
$q->whereIn('id', $transformed_ids);
});
})
->orderBy('is_default', 'DESC')
->get();
$filtered_gateways = $gateway_tokens->filter(function ($gateway_token) use ($amount) {

View File

@@ -450,7 +450,7 @@ class TaxPeriodReport extends BaseExport
$this->data['invoices'][] = $invoice_row_builder->build();
// Build and add invoice item rows for each tax detail
foreach ($event->metadata->tax_report->tax_details as $tax_detail_data) {
foreach ($event->metadata->tax_report->tax_details ?? [] as $tax_detail_data) {
$tax_detail = TaxDetail::fromMetadata($tax_detail_data);
$item_row_builder = new InvoiceItemReportRow(

View File

@@ -937,11 +937,10 @@ Código seguro de verificación (CSV): {$verifactu_log->status}";
$tax_label = '';
if (collect($this->entity->line_items)->contains('tax_id', \App\Models\Product::PRODUCT_TYPE_REVERSE_TAX)) {
$tax_label .= ctrans('texts.reverse_tax_info') . "<br>";
$tax_label .= ctrans('texts.reverse_tax_info') . " <br>";
}
if ((int)$this->client->country_id !== (int)$this->company->settings->country_id) {
$tax_label .= ctrans('texts.intracommunity_tax_info') . "<br>";
else if ((int)$this->client->country_id !== (int)$this->company->settings->country_id) {
$tax_label .= ctrans('texts.intracommunity_tax_info') . " <br>";
if ($this->entity_calc->getTotalTaxes() > 0) {
$tax_label = '';

View File

@@ -17,8 +17,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => env('APP_VERSION', '5.12.45'),
'app_tag' => env('APP_TAG', '5.12.45'),
'app_version' => env('APP_VERSION', '5.12.46'),
'app_tag' => env('APP_TAG', '5.12.46'),
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),

View File

@@ -5687,6 +5687,8 @@ $lang = array(
'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date',
'notification_invoice_overdue_summary' => 'The following invoices are overdue:',
'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?',
'peppol_sending_failed' => 'E-Invoice sending failed!',
'peppol_sending_success' => 'E-Invoice sent successfully!',
);
return $lang;

View File

@@ -4688,6 +4688,8 @@ Lorsque les montant apparaîtront sur votre relevé, veuillez revenir sur cette
'show_tasks_in_client_portal' => 'Afficher les tâches sur le portail du client',
'notification_quote_expired_subject' => 'La soumission :invoice a expiré pour :client',
'notification_quote_expired' => 'La soumission :invoice pour le client :client au montant de :amount est expirée',
'notification_invoice_overdue_subject' => 'La facture :invoice est échue pour :client',
'notification_invoice_overdue' => 'La facture :invoice pour le client :client de :amount est échue.',
'auto_sync' => 'Synchronisation automatique',
'refresh_accounts' => 'Rafraîchir les comptes',
'upgrade_to_connect_bank_account' => 'Passer au plan Entreprise pour connecter votre compte bancaire',
@@ -5634,7 +5636,6 @@ Développe automatiquement la section des notes dans le tableau de produits pour
'einvoice_received_subject' => 'E-facture(s) reçues',
'einvoice_received_body' => 'Vous avez reçu :count nouvelle(s) E-facture(s).<br><br>Connectez-vous pour les consulter.',
'download_files_too_large' => 'La taille de certains fichiers dépassait la limite pour être joints directement au message courriel.',
'restore_disabled_verifactu' => 'Vous ne pouvez pas restaurer une facture une fois qu\'elle a été supprimée.',
'delete_disabled_verifactu' => 'Vous ne pouvez pas supprimer une facture une fois qu\'elle a été annulée ou modifiée.',
'rectify' => 'Rectificar',
@@ -5643,10 +5644,6 @@ Développe automatiquement la section des notes dans le tableau de produits pour
'verifactu_cancellation_send_success' => 'Annulation de facture pour :invoice envoyée à AEAT',
'verifactu_cancellation_send_failure' => 'Annulation de facture pour :invoice n\'a pas été envoyée AEAT :notes',
'verifactu' => 'Verifactu',
'activity_150' => 'Compte supprimé :notes',
'activity_151' => 'Le client :notes a été fusionné avec :client par :user',
'activity_152' => 'Le fournisseur :notes a été fusionné avec :vendor par :user',
'activity_153' => 'Le client :notes a été purgé par :user',
'justify' => 'Justifier',
'outdent' => 'Désindenter',
'indent' => 'Indenter',
@@ -5665,6 +5662,30 @@ Développe automatiquement la section des notes dans le tableau de produits pour
'thank_you_for_feedback' => 'Merci pour vos commentaires !',
'use_legacy_editor' => 'Utiliser l\'éditeur classique Wysiwyg',
'use_legacy_editor_help' => 'Utiliser l\'éditeur TinyMCE.',
'enable_e_invoice_received_notification' => 'Activer la notification de réception de facture électronique',
'enable_e_invoice_received_notification_help' => 'Recevoir une notification par courriel lorsqu\'une nouvelle facture électronique est reçue.',
'price_changes' => 'Changements de prix des forfaits à partir du 1er janvier 2026',
'notification_quote_rejected_subject' => 'La soumission :quote n\'a pas été acceptée par :client',
'notification_quote_rejected' => 'Le client :client n\'a pas accepté la soumission :quote pour :amount :notes.',
'activity_150' => 'Compte supprimé :notes',
'activity_151' => 'Le client :notes a été fusionné avec :client par :user',
'activity_152' => 'Le fournisseur :notes a été fusionné avec :vendor par :user',
'activity_153' => 'Le client :notes a été purgé par :user',
'activity_154' => 'La facture électronique :invoice pour :client a été envoyer à AEAT.',
'activity_155' => 'L\'envoi de la facture électronique :invoice pour :client à l\'AEAT a échoué :notes',
'activity_156' => 'L\'annulation de la facture pour :invoice a été envoyée',
'activity_157' => 'L\'envoi de l\'annulation de la facture pour :invoice a échoué pour AEAT :notes',
'activity_158' => 'La soumission :quote n\'a pas été accepté par :client :notes',
'quotes_with_status_sent_can_be_rejected' => 'Seules les soumission avec le statut "Envoyée" peuvent être rejetées.',
'reject' => 'Rejeter',
'rejected' => 'Rejeté',
'reject_quote' => 'Rejeter la soumission',
'reject_quote_confirmation' => 'Êtes-vous sûr de vouloir rejeter cette soumission ?',
'reason' => 'Raison',
'enter_reason' => 'Préciser la raison...',
'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date',
'notification_invoice_overdue_summary' => 'The following invoices are overdue:',
'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?',
);
return $lang;

View File

@@ -3831,9 +3831,9 @@ Kom terug naar deze betaalmethode pagina zodra u de bedragen heeft ontvangen en
'to_view_entity_password' => 'Om de :entity te bekijken moet u een wachtwoord invoeren.',
'showing_x_of' => 'Toont de :first tot :last van de :total resultaten',
'no_results' => 'Geen resultaten gevonden.',
'payment_failed_subject' => 'Betaling mislukt voor klant :klant',
'payment_failed_subject' => 'Betaling mislukt voor klant :client',
'payment_failed_body' => 'Een betaling gedaan door de klant :client is mislukt met bericht :bericht',
'register' => 'Registreer',
'register' => 'Registreren',
'register_label' => 'Maak binnen enkele seconden uw account aan',
'password_confirmation' => 'Bevestig uw wachtwoord',
'verification' => 'Verificatie',
@@ -3925,8 +3925,8 @@ Kom terug naar deze betaalmethode pagina zodra u de bedragen heeft ontvangen en
'invoice_number_taken' => 'Factuurnummer reeds in gebruik',
'payment_id_required' => 'Betalings-id verplicht',
'unable_to_retrieve_payment' => 'Niet in staat om gevraagde betaling op te halen',
'invoice_not_related_to_payment' => 'Factuur ID :invoice is niet herleidbaar naar deze betaling',
'credit_not_related_to_payment' => 'Creditfactuur ID :credit is niet verwant aan deze betaling',
'invoice_not_related_to_payment' => 'Factuur # :invoice is niet gerelateerd aan deze betaling.',
'credit_not_related_to_payment' => 'Krediet # :credit is niet gerelateerd aan deze betaling.',
'max_refundable_invoice' => 'Poging tot terugbetaling is groter dan toegestaan voor invoice id :invoice, maximum terug te betalen bedrag is :amount',
'refund_without_invoices' => 'Wanneer u een betaling met bijgevoegde facturen wilt terugbetalen, geef dan aan welke geldige factuur/facturen u wilt terugbetalen.',
'refund_without_credits' => 'Wanneer u een betaling met bijgevoegde tegoeden wilt terugbetalen, geef dan aan welke tegoeden geldig zijn en u deze wilt terugbetalen.',
@@ -4691,6 +4691,8 @@ E-mail: :email<b><br><b>',
'show_tasks_in_client_portal' => 'Toon taken in klantenportaal',
'notification_quote_expired_subject' => 'Offerte :invoice is verlopen voor :client',
'notification_quote_expired' => 'De volgende Offerte :invoice voor klant :client en :amount is nu verlopen.',
'notification_invoice_overdue_subject' => 'Invoice :invoice is overdue for :client',
'notification_invoice_overdue' => 'The following Invoice :invoice for client :client and :amount is now overdue.',
'auto_sync' => 'Automatisch synchroniseren',
'refresh_accounts' => 'Ververs accounts',
'upgrade_to_connect_bank_account' => 'Upgrade naar Enterprise om uw bankrekening te koppelen',
@@ -4946,7 +4948,7 @@ E-mail: :email<b><br><b>',
'here' => 'hier',
'industry_Restaurant & Catering' => 'Restaurant & Horeca',
'show_credits_table' => 'Credittabel tonen',
'manual_payment' => 'Betalingshandleiding',
'manual_payment' => 'Handmatige betaling',
'tax_summary_report' => 'Fiscaal overzichtsrapport',
'tax_category' => 'Belastingcategorie',
'physical_goods' => 'Fysieke goederen',
@@ -5186,7 +5188,7 @@ E-mail: :email<b><br><b>',
'step_authentication_fail' => 'U moet ten minste één van de authenticatiemethoden opnemen.',
'auth.login' => 'Log in',
'auth.login-or-register' => 'Log in of registreer',
'auth.register' => 'Register',
'auth.register' => 'Registreren',
'cart' => 'Winkelwagen',
'methods' => 'Methoden',
'rff' => 'Verplichte velden formulier',
@@ -5366,7 +5368,7 @@ E-mail: :email<b><br><b>',
'step' => 'Stap',
'peppol_whitelabel_warning' => 'Voor het gebruik van e-facturatie via het PEPPOL-netwerk is een whitelabellicentie vereist.',
'peppol_plan_warning' => 'Voor het gebruik van e-facturatie via het PEPPOL-netwerk is een Enterprise-abonnement vereist.',
'peppol_credits_info' => 'Ecredits zijn vereist om e-facturen te versturen en ontvangen. Deze worden per document in rekening gebracht.',
'peppol_credits_info' => 'Ecredits are required to send and receive einvoices. These are charged on a per document basis. If you already have credits, click Continue.',
'buy_credits' => 'Koop E-credits',
'peppol_successfully_configured' => 'PEPPOL succesvol geconfigureerd.',
'peppol_not_paid_message' => 'Enterprise-abonnement vereist voor PEPPOL. Upgrade uw abonnement.',
@@ -5615,6 +5617,76 @@ E-mail: :email<b><br><b>',
'tax_nexus' => 'Belastingnexus',
'tax_period_report' => 'Belastingperioderapport',
'creator' => 'Gemaakt door',
'ses_topic_arn_help' => 'The SES topic (optional, only for webhook tracking)',
'ses_region_help' => 'The AWS region, ie us-east-1',
'ses_secret_key' => 'SES Secret Key',
'ses_access_key' => 'SES Access Key ID',
'activity_151' => 'Klant :notes is samengevoegd met :client door :user',
'activity_152' => 'Leverancier :notes is samengevoegd met :vendor door :user',
'activity_153' => 'Klant :notes verwijderd door :user',
'lifecycle' => 'Lifecycle',
'order_columns' => 'Sorteer kolommen',
'topic_arn' => 'Topic ARN',
'lang_Catalan' => 'Catalan',
'lang_Afrikaans' => 'Afrikaans',
'lang_Indonesian' => 'Indonesian',
'replaced' => 'Replaced',
'ses_from_address' => 'SES From Address',
'ses_from_address_help' => 'The Sending Email Address, must be verified in AWS',
'unauthorized_action' => 'U bent niet gemachtigd om deze actie uit te voeren.',
'einvoice_received_subject' => 'E-Invoice/s Received',
'einvoice_received_body' => 'You have received :count new E-Invoice/s.<br><br>Login to view.',
'download_files_too_large' => 'Some files were too large to attach directly to the email. Please use the links below to download these individually.',
'restore_disabled_verifactu' => 'You cannot restore an invoice once it has been deleted',
'delete_disabled_verifactu' => 'You cannot delete an invoice once it has been cancelled or modified',
'rectify' => 'Rectificar',
'verifactu_invoice_send_success' => 'Invoice :invoice for :client sent to AEAT successfully',
'verifactu_invoice_sent_failure' => 'Invoice :invoice for :client failed to send to AEAT :notes',
'verifactu_cancellation_send_success' => 'Invoice cancellation for :invoice sent to AEAT successfully',
'verifactu_cancellation_send_failure' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
'verifactu' => 'Verifactu',
'justify' => 'Justify',
'outdent' => 'Outdent',
'indent' => 'Indent',
'clear_filters' => 'Clear Filters',
'feedback' => 'Feedback',
'feedback_modal_description' => 'We would love to hear your feedback!',
'do_not_ask_again' => 'Do not ask again',
'not_likely' => 'Not likely',
'extremely_likely' => 'Extremely likely',
'feedback_slider_title' => 'How likely are you to recommend Invoice Ninja to a friend or colleague?',
'actual_delivery_date' => 'Actual Delivery Date',
'actual_delivery_date_help' => 'Sometimes required when billing across borders. Defines the EXACT date of delivery of goods.',
'invoice_period' => 'Invoice Period',
'invoice_period_help' => 'Defines the time period for which the services were provided.',
'paused_recurring_invoice_helper' => 'Caution! When restarting a recurring invoice, ensure the next send date is in the future.',
'thank_you_for_feedback' => 'Thank you for your feedback!',
'use_legacy_editor' => 'Use Legacy Wysiwyg Editor',
'use_legacy_editor_help' => 'Use the TinyMCE editor.',
'enable_e_invoice_received_notification' => 'Enable E-Invoice Received Notification',
'enable_e_invoice_received_notification_help' => 'Receive an email notification when a new E-Invoice is received.',
'price_changes' => 'Plan Price Changes from January 1st 2026',
'notification_quote_rejected_subject' => 'Offerte :quote werd afgewezen door :client',
'notification_quote_rejected' => 'The following client :client rejected Quote :quote for :amount :notes.',
'activity_150' => 'Account verwijderd :notes',
'activity_151' => 'Klant :notes is samengevoegd met :client door :user',
'activity_152' => 'Leverancier :notes is samengevoegd met :vendor door :user',
'activity_153' => 'Klant :notes verwijderd door :user',
'activity_154' => 'E-Invoice :invoice for :client sent to AEAT successfully',
'activity_155' => 'E-Invoice :invoice for :client failed to send to AEAT :notes',
'activity_156' => 'Invoice cancellation for :invoice sent to AEAT successfully',
'activity_157' => 'Invoice cancellation for :invoice failed to send to AEAT :notes',
'activity_158' => 'Quote :quote was rejected by :client :notes',
'quotes_with_status_sent_can_be_rejected' => 'Only quotes with "Sent" status can be rejected.',
'reject' => 'Reject',
'rejected' => 'Rejected',
'reject_quote' => 'Reject Quote',
'reject_quote_confirmation' => 'Are you sure you want to reject this quote?',
'reason' => 'Reden',
'enter_reason' => 'Geef een reden op...',
'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date',
'notification_invoice_overdue_summary' => 'The following invoices are overdue:',
'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?',
);
return $lang;

View File

@@ -4691,6 +4691,8 @@ $lang = array(
'show_tasks_in_client_portal' => 'Hiển thị nhiệm vụ trong Cổng thông tin khách hàng',
'notification_quote_expired_subject' => 'Báo giá :invoice đã hết hạn cho :client',
'notification_quote_expired' => 'Báo giá :invoice sau đây dành cho khách hàng :client và :amount hiện đã hết hạn.',
'notification_invoice_overdue_subject' => 'Hóa đơn :invoice quá hạn cho :client',
'notification_invoice_overdue' => 'Hóa đơn :invoice cho khách hàng :client và :amount hiện đã quá hạn.',
'auto_sync' => 'Tự động đồng bộ',
'refresh_accounts' => 'Làm mới tài khoản',
'upgrade_to_connect_bank_account' => 'Nâng cấp lên Enterprise để kết nối tài khoản ngân hàng của bạn',
@@ -5635,7 +5637,6 @@ $lang = array(
'einvoice_received_subject' => 'E- Hóa đơn /s Received',
'einvoice_received_body' => 'Bạn đã nhận được :count mới E- Hóa đơn /s.<br><br> Đăng nhập đến Xem .',
'download_files_too_large' => 'Một số tệp quá lớn đến không thể đính kèm trực tiếp đến email . Vui lòng sử dụng các liên kết bên dưới đến tải xuống từng tệp riêng lẻ.',
'restore_disabled_verifactu' => 'Bạn không thể Khôi phục Hóa đơn một khi nó đã bị đã xóa',
'delete_disabled_verifactu' => 'Bạn không thể Xóa một Hóa đơn sau khi nó đã bị hủy hoặc sửa đổi',
'rectify' => 'Rectificar',
@@ -5644,10 +5645,6 @@ $lang = array(
'verifactu_cancellation_send_success' => 'Hóa đơn hủy :invoice đã gửi đến AEAT Thành công',
'verifactu_cancellation_send_failure' => 'Hóa đơn hủy cho :invoice không gửi đến đến AEAT :notes',
'verifactu' => 'Verifactu',
'activity_150' => 'tài khoản đã xóa :notes',
'activity_151' => 'Khách hàng :notes merged into :client by :user',
'activity_152' => 'Người bán :notes merged into :vendor by :user',
'activity_153' => 'Khách hàng :notes bị :user thanh lọc',
'justify' => 'Căn chỉnh',
'outdent' => 'Lồi ra ngoài',
'indent' => 'thụt lề',
@@ -5666,6 +5663,30 @@ $lang = array(
'thank_you_for_feedback' => 'Cảm ơn phản hồi của bạn!',
'use_legacy_editor' => 'Sử dụng Legacy Wysiwyg Editor',
'use_legacy_editor_help' => 'Sử dụng trình soạn thảo TinyMCE.',
'enable_e_invoice_received_notification' => 'Enable E- Hóa đơn Đã nhận Thông báo',
'enable_e_invoice_received_notification_help' => 'Nhận thông báo email khi nhận được Hóa đơn mới .',
'price_changes' => 'Thay đổi giá gói dịch vụ từ ngày 1 tháng 1 năm 2026',
'notification_quote_rejected_subject' => 'báo giá :quote đã bị từ chối bởi :client',
'notification_quote_rejected' => 'khách hàng sau đây :client đã từ chối báo giá :quote cho :amount :notes .',
'activity_150' => 'tài khoản đã xóa :notes',
'activity_151' => 'Khách hàng :notes merged into :client by :user',
'activity_152' => 'Người bán :notes merged into :vendor by :user',
'activity_153' => 'Khách hàng :notes bị :user thanh lọc',
'activity_154' => 'E- Hóa đơn :invoice for :client TXEND gửi đến AEAT Thành công',
'activity_155' => 'E- Hóa đơn :invoice cho :client không đến được đến AEAT :notes',
'activity_156' => 'Hóa đơn hủy :invoice đã gửi đến AEAT Thành công',
'activity_157' => 'Hóa đơn hủy cho :invoice không đến được đến AEAT :notes',
'activity_158' => 'báo giá :quote đã bị từ chối bởi :client :notes',
'quotes_with_status_sent_can_be_rejected' => 'Chỉ Báo giá có trạng thái &quot;Đã gửi&quot; mới bị từ chối.',
'reject' => 'Từ chối',
'rejected' => 'Vật bị loại bỏ',
'reject_quote' => 'Từ chối báo giá',
'reject_quote_confirmation' => 'Bạn có chắc chắn muốn đến chối báo giá này không?',
'reason' => 'Lý do',
'enter_reason' => 'Nhập một lý do...',
'notification_invoice_overdue_summary_subject' => 'Invoice Overdue Summary: :date',
'notification_invoice_overdue_summary' => 'The following invoices are overdue:',
'purge_user_confirmation' => 'Warning! This action will reassign all entities to the account owner and permanently delete the user across all companies and accounts. Are you sure you want to proceed?',
);
return $lang;

View File

@@ -30,6 +30,7 @@ parameters:
- \Stripe\Collection
reportUnmatchedIgnoredErrors: false
ignoreErrors:
- '#\Saxon\SaxonProcessor#'
- '#Array has 2 duplicate keys with value#'
- '#Call to an undefined method#'
- '#makeHidden#'

View File

@@ -178,7 +178,7 @@ input:checked ~ .dot {
Enterprise Plan
</h3>
<p class="text-5xl font-bold text-center text-white" id="y_plan_price">
$140
$180
</p>
<p class="text-xs text-center uppercase text-white">
yearly
@@ -275,12 +275,12 @@ document.getElementById('handleProYearlyClick').addEventListener('click', functi
});
const price_map = new Map();
//monthly
price_map.set('7LDdwRb1YK', '$16');
price_map.set('7LDdwRb1YK', '$18');
price_map.set('MVyb8mdvAZ', '$32');
price_map.set('WpmbkR5azJ', '$54');
price_map.set('k8mepY2aMy', '$84');
//yearly
price_map.set('LYqaQWldnj', '$160');
price_map.set('LYqaQWldnj', '$180');
price_map.set('kQBeX6mbyK', '$320');
price_map.set('GELe32Qd69', '$540');
price_map.set('MVyb86oevA', '$840');

View File

@@ -45,6 +45,44 @@ class CompanyGatewayApiTest extends TestCase
Model::reguard();
}
public function testCompanyGatewayIdsUpdateWhenAddingNewGateway()
{
$settings = $this->company->settings;
$settings->company_gateway_ids = "Xe0Vjm5ybx,Xe00Aw9Lex,Xe0RpmK3Gb";
$this->company->settings = $settings;
$this->company->save();
$this->assertEquals("Xe0Vjm5ybx,Xe00Aw9Lex,Xe0RpmK3Gb", $this->company->getSetting('company_gateway_ids'));
$data = [
'config' => 'random config',
'gateway_key' => '3b6621f970ab18887c4f6dca78d3f8bb',
];
/* POST */
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/company_gateways', $data);
$cg = $response->json();
$cg_id = $cg['data']['id'];
$this->assertNotNull($cg_id);
$response->assertStatus(200);
$company = $this->company->fresh();
$settings = $company->settings;
$this->assertCount(4, explode(',', $company->getSetting('company_gateway_ids')));
$this->assertStringContainsString($cg_id, $company->getSetting('company_gateway_ids'));
}
public function testBulkActions()
{
$cg = CompanyGatewayFactory::create($this->company->id, $this->user->id);

View File

@@ -12,12 +12,10 @@
namespace Tests\Feature;
use App\Jobs\Invoice\CheckGatewayFee;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\MockAccountData;
use Tests\TestCase;

View File

@@ -1134,7 +1134,7 @@ class ReportCsvGenerationTest extends TestCase
$csv = $response->body();
$reader = Reader::createFromString($csv);
$reader = Reader::fromString($csv);
$reader->setHeaderOffset(0);
$res = $reader->fetchColumnByName('Street');
@@ -1983,7 +1983,7 @@ class ReportCsvGenerationTest extends TestCase
$csv = $response->body();
$reader = Reader::createFromString($csv);
$reader = Reader::fromString($csv);
$reader->setHeaderOffset(0);
$res = $reader->fetchColumnByName('Contact First Name');
@@ -2014,7 +2014,7 @@ class ReportCsvGenerationTest extends TestCase
private function getFirstValueByColumn($csv, $column)
{
$reader = Reader::createFromString($csv);
$reader = Reader::fromString($csv);
$reader->setHeaderOffset(0);
$res = $reader->fetchColumnByName($column);