diff --git a/.dev/adminer/Dockerfile b/.dev/adminer/Dockerfile index e858b2a2..12d293a5 100644 --- a/.dev/adminer/Dockerfile +++ b/.dev/adminer/Dockerfile @@ -2,12 +2,6 @@ FROM adminer:latest USER root -RUN set -x && \ - apt update && \ - apt install curl -y && \ - cd /var/www/html/plugins-enabled && \ - curl -O https://gist.githubusercontent.com/gdarko/00af6e9a754f09c3f81cd3c606c33311/raw/d5f6a30f00edecf30a5d380340d9dae79a3b7352/login-password-less.php - USER adminer CMD [ "php", "-S", "[::]:8080", "-t", "/var/www/html" ] diff --git a/.env.example b/.env.example index 699b6495..c78d9e93 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ APP_DEBUG=true APP_NAME="InvoiceShelf" APP_LOG_LEVEL=debug APP_TIMEZONE=UTC -APP_URL=http://invoiceshelf.test +APP_URL= APP_LOCALE=en APP_FALLBACK_LOCALE=en @@ -49,7 +49,6 @@ PUSHER_APP_ID= PUSHER_KEY= PUSHER_SECRET= -SANCTUM_STATEFUL_DOMAINS=invoiceshelf.test TRUSTED_PROXIES="*" CRON_JOB_AUTH_TOKEN="" diff --git a/app/Http/Controllers/V1/Installation/DatabaseConfigurationController.php b/app/Http/Controllers/V1/Installation/DatabaseConfigurationController.php index 5c640349..c66cab1c 100644 --- a/app/Http/Controllers/V1/Installation/DatabaseConfigurationController.php +++ b/app/Http/Controllers/V1/Installation/DatabaseConfigurationController.php @@ -29,7 +29,10 @@ class DatabaseConfigurationController extends Controller $results = $this->environmentManager->saveDatabaseVariables($request); if (array_key_exists('success', $results)) { - Artisan::call('key:generate --force'); + // Automatically regenerating the key is disabled to prevent complications in the wizard process. + // This can cause issues with the CSRF token, resulting in "Token Mismatch" or "Invalid CSRF Token" errors. + // It is recommended that the user manually generates the key before running the wizard to ensure application security and stability. + // Artisan::call('key:generate --force'); Artisan::call('optimize:clear'); Artisan::call('config:clear'); Artisan::call('cache:clear'); diff --git a/app/Http/Resources/EstimateResource.php b/app/Http/Resources/EstimateResource.php index 67766ab4..0b0eac22 100644 --- a/app/Http/Resources/EstimateResource.php +++ b/app/Http/Resources/EstimateResource.php @@ -21,6 +21,7 @@ class EstimateResource extends JsonResource 'status' => $this->status, 'reference_number' => $this->reference_number, 'tax_per_item' => $this->tax_per_item, + 'tax_included' => $this->tax_included, 'discount_per_item' => $this->discount_per_item, 'notes' => $this->getNotes(), 'discount' => $this->discount, diff --git a/app/Http/Resources/InvoiceResource.php b/app/Http/Resources/InvoiceResource.php index 5aaf3fd5..e5d76589 100644 --- a/app/Http/Resources/InvoiceResource.php +++ b/app/Http/Resources/InvoiceResource.php @@ -22,6 +22,7 @@ class InvoiceResource extends JsonResource 'status' => $this->status, 'paid_status' => $this->paid_status, 'tax_per_item' => $this->tax_per_item, + 'tax_included' => $this->tax_included, 'discount_per_item' => $this->discount_per_item, 'notes' => $this->notes, 'discount_type' => $this->discount_type, diff --git a/app/Http/Resources/RecurringInvoiceResource.php b/app/Http/Resources/RecurringInvoiceResource.php index 8aaed699..fd7352c8 100644 --- a/app/Http/Resources/RecurringInvoiceResource.php +++ b/app/Http/Resources/RecurringInvoiceResource.php @@ -32,6 +32,7 @@ class RecurringInvoiceResource extends JsonResource 'limit_date' => $this->limit_date, 'exchange_rate' => $this->exchange_rate, 'tax_per_item' => $this->tax_per_item, + 'tax_included' => $this->tax_included, 'discount_per_item' => $this->discount_per_item, 'notes' => $this->notes, 'discount_type' => $this->discount_type, diff --git a/app/Jobs/CreateBackupJob.php b/app/Jobs/CreateBackupJob.php index f21fba98..8504f7c1 100644 --- a/app/Jobs/CreateBackupJob.php +++ b/app/Jobs/CreateBackupJob.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Spatie\Backup\Config\Config; use Spatie\Backup\Tasks\Backup\BackupJobFactory; class CreateBackupJob implements ShouldQueue @@ -41,7 +42,8 @@ class CreateBackupJob implements ShouldQueue config(['backup.backup.destination.disks' => [$prefix.$fileDisk->driver]]); - $backupJob = BackupJobFactory::createFromArray(config('backup')); + $config = Config::fromArray(config('backup')); + $backupJob = BackupJobFactory::createFromConfig($config); if (! defined('SIGINT')) { $backupJob->disableSignals(); } diff --git a/app/Models/Payment.php b/app/Models/Payment.php index ac9506c6..763a880b 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -459,7 +459,7 @@ class Payment extends Model implements HasMedia ->setNextNumbers(); $data['payment_number'] = $serial->getNextNumber(); - $data['payment_date'] = Carbon::now()->format('y-m-d'); + $data['payment_date'] = Carbon::now(); $data['amount'] = $invoice->total; $data['invoice_id'] = $invoice->id; $data['payment_method_id'] = request()->payment_method_id; diff --git a/app/Space/EnvironmentManager.php b/app/Space/EnvironmentManager.php index 05d2cbb5..2e3aea14 100755 --- a/app/Space/EnvironmentManager.php +++ b/app/Space/EnvironmentManager.php @@ -104,14 +104,22 @@ class EnvironmentManager */ public function saveDatabaseVariables(DatabaseEnvironmentRequest $request) { + $appUrl = $request->get('app_url'); + if ($appUrl !== config('app.url')) { + config(['app.url' => $appUrl]); + } + [$sanctumDomain, $sessionDomain] = $this->getDomains( + $request->getHttpHost() + ); $dbEnv = [ - 'APP_URL' => $request->get('app_url'), + 'APP_URL' => $appUrl, 'APP_LOCALE' => $request->get('app_locale'), 'DB_CONNECTION' => $request->get('database_connection'), - 'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'), - 'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0], + 'SESSION_DOMAIN' => $sessionDomain, ]; - + if ($sanctumDomain !== null) { + $dbEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain; + } if ($dbEnv['DB_CONNECTION'] != 'sqlite') { if ($request->has('database_username') && $request->has('database_password')) { $dbEnv['DB_HOST'] = $request->get('database_hostname'); @@ -462,10 +470,16 @@ class EnvironmentManager public function saveDomainVariables(DomainEnvironmentRequest $request) { try { - $this->updateEnv([ - 'SANCTUM_STATEFUL_DOMAINS' => $request->get('app_domain'), - 'SESSION_DOMAIN' => explode(':', $request->get('app_domain'))[0], - ]); + [$sanctumDomain, $sessionDomain] = $this->getDomains( + $request->get('app_domain') + ); + $domainEnv = [ + 'SESSION_DOMAIN' => $sessionDomain, + ]; + if ($sanctumDomain !== null) { + $domainEnv['SANCTUM_STATEFUL_DOMAINS'] = $sanctumDomain; + } + $this->updateEnv($domainEnv); } catch (Exception $e) { return [ 'error' => 'domain_verification_failed', @@ -505,4 +519,25 @@ class EnvironmentManager file_put_contents($this->envPath, trim($formatted)); } + + private function getDomains(string $requestDomain): array + { + $appUrl = config('app.url'); + + $port = parse_url($appUrl, PHP_URL_PORT); + $currentDomain = parse_url($appUrl, PHP_URL_HOST).( + $port ? ':'.$port : '' + ); + + $requestHost = parse_url($requestDomain, PHP_URL_HOST) ?: $requestDomain; + + $isSame = $currentDomain === $requestDomain; + + return [ + $isSame && env('SANCTUM_STATEFUL_DOMAINS', false) === false ? + null : $requestDomain, + $isSame && env('SESSION_DOMAIN', false) === null ? + null : $requestHost, + ]; + } } diff --git a/composer.lock b/composer.lock index 730429a3..752a47a8 100644 --- a/composer.lock +++ b/composer.lock @@ -12363,4 +12363,4 @@ }, "platform-dev": {}, "plugin-api-version": "2.6.0" -} +} \ No newline at end of file diff --git a/config/backup.php b/config/backup.php index e04e7ad2..0181382a 100644 --- a/config/backup.php +++ b/config/backup.php @@ -26,6 +26,7 @@ return [ 'exclude' => [ base_path('vendor'), base_path('node_modules'), + base_path('.git'), ], /* diff --git a/config/sanctum.php b/config/sanctum.php deleted file mode 100644 index 8c94e2a9..00000000 --- a/config/sanctum.php +++ /dev/null @@ -1,48 +0,0 @@ - explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')), - - /* - |-------------------------------------------------------------------------- - | Expiration Minutes - |-------------------------------------------------------------------------- - | - | This value controls the number of minutes until an issued token will be - | considered expired. If this value is null, personal access tokens do - | not expire. This won't tweak the lifetime of first-party sessions. - | - */ - - 'expiration' => null, - - /* - |-------------------------------------------------------------------------- - | Sanctum Middleware - |-------------------------------------------------------------------------- - | - | When authenticating your first-party SPA with Sanctum you may need to - | customize some of the middleware Sanctum uses while processing the - | request. You may change the middleware listed below as required. - | - */ - - 'middleware' => [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => Illuminate\Cookie\Middleware\EncryptCookies::class, - 'validate_csrf_token' => Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, - ], - -]; diff --git a/database/factories/EstimateFactory.php b/database/factories/EstimateFactory.php index ddcdb2ed..3db92cfd 100644 --- a/database/factories/EstimateFactory.php +++ b/database/factories/EstimateFactory.php @@ -93,6 +93,7 @@ class EstimateFactory extends Factory return $estimate['discount_type'] == 'percentage' ? (($estimate['discount_val'] * $estimate['total']) / 100) : $estimate['discount_val']; }, 'tax_per_item' => 'YES', + 'tax_included' => false, 'discount_per_item' => 'No', 'tax' => $this->faker->randomDigitNotNull(), 'notes' => $this->faker->text(80), diff --git a/database/factories/InvoiceFactory.php b/database/factories/InvoiceFactory.php index 485e627e..a09a5bdc 100644 --- a/database/factories/InvoiceFactory.php +++ b/database/factories/InvoiceFactory.php @@ -93,6 +93,7 @@ class InvoiceFactory extends Factory 'template_name' => 'invoice1', 'status' => Invoice::STATUS_DRAFT, 'tax_per_item' => 'NO', + 'tax_included' => false, 'discount_per_item' => 'NO', 'paid_status' => Invoice::STATUS_UNPAID, 'company_id' => User::find(1)->companies()->first()->id, diff --git a/database/factories/RecurringInvoiceFactory.php b/database/factories/RecurringInvoiceFactory.php index 96304a6b..61e5e40d 100644 --- a/database/factories/RecurringInvoiceFactory.php +++ b/database/factories/RecurringInvoiceFactory.php @@ -26,6 +26,7 @@ class RecurringInvoiceFactory extends Factory 'send_automatically' => false, 'status' => $this->faker->randomElement(['COMPLETED', 'ON_HOLD', 'ACTIVE']), 'tax_per_item' => 'NO', + 'tax_included' => false, 'discount_per_item' => 'NO', 'sub_total' => $this->faker->randomDigitNotNull(), 'total' => $this->faker->randomDigitNotNull(), diff --git a/database/migrations/2025_05_04_152240_add_tax_included_to_invoices.php b/database/migrations/2025_05_04_152240_add_tax_included_to_invoices.php new file mode 100644 index 00000000..d95137b5 --- /dev/null +++ b/database/migrations/2025_05_04_152240_add_tax_included_to_invoices.php @@ -0,0 +1,28 @@ +boolean('tax_included')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn('tax_included'); + }); + } +}; diff --git a/database/migrations/2025_05_04_152522_add_tax_included_to_estimates.php b/database/migrations/2025_05_04_152522_add_tax_included_to_estimates.php new file mode 100644 index 00000000..dfa08c96 --- /dev/null +++ b/database/migrations/2025_05_04_152522_add_tax_included_to_estimates.php @@ -0,0 +1,28 @@ +boolean('tax_included')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('estimates', function (Blueprint $table) { + $table->dropColumn('tax_included'); + }); + } +}; diff --git a/database/migrations/2025_05_04_152833_add_tax_included_to_recurring_invoices.php b/database/migrations/2025_05_04_152833_add_tax_included_to_recurring_invoices.php new file mode 100644 index 00000000..e22691ef --- /dev/null +++ b/database/migrations/2025_05_04_152833_add_tax_included_to_recurring_invoices.php @@ -0,0 +1,28 @@ +boolean('tax_included')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('recurring_invoices', function (Blueprint $table) { + $table->dropColumn('tax_included'); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 84650b9d..cb96f9a4 100644 --- a/lang/en.json +++ b/lang/en.json @@ -306,6 +306,7 @@ "total": "Total", "discount": "Discount", "sub_total": "Sub Total", + "net_total": "Net", "estimate_number": "Estimate Number", "ref_number": "Ref Number", "contact": "Contact", @@ -1220,7 +1221,11 @@ "updated_message": "Tax type updated successfully", "deleted_message": "Tax type deleted successfully", "confirm_delete": "You will not be able to recover this Tax Type", - "already_in_use": "Tax is already in use" + "already_in_use": "Tax is already in use", + "tax_included": "Inclusive taxes", + "tax_included_description": "Enable this if you want to report that taxes are already included in the invoice items or invoice total.", + "tax_included_by_default": "Enable inclusive taxes by default", + "tax_included_by_default_description": "Enable this if you want to set inclusive taxes by default" }, "payment_modes": { "title": "Payment Modes", @@ -1616,6 +1621,7 @@ "pdf_discount_label": "Discount", "pdf_amount_label": "Amount", "pdf_subtotal": "Subtotal", + "pdf_net_total": "Net", "pdf_total": "Total", "pdf_payment_label": "Payment", "pdf_payment_receipt_label": "PAYMENT RECEIPT", diff --git a/lang/es.json b/lang/es.json index 7dbbacdd..9e60dfca 100644 --- a/lang/es.json +++ b/lang/es.json @@ -303,6 +303,7 @@ "total": "Total", "discount": "Descuento", "sub_total": "Subtotal", + "net_total": "Base Imponible", "estimate_number": "Número de Presupuesto", "ref_number": "Número de referencia", "contact": "Contacto", @@ -1212,7 +1213,11 @@ "updated_message": "Tipo de impuesto actualizado correctamente", "deleted_message": "Tipo de impuesto eliminado correctamente", "confirm_delete": "No podrá recuperar este tipo de impuesto", - "already_in_use": "El impuesto ya está en uso." + "already_in_use": "El impuesto ya está en uso.", + "tax_included": "Impuestos inclusivos", + "tax_included_description": "Habilítelo si desea informar que los impuestos ya están incluidos en los artículos de la factura o en el total de la factura.", + "tax_included_by_default": "Usar impuestos inclusivos por defecto", + "tax_included_by_default_description": "Habilítelo si desea establecer los impuestos inclusivos por defecto." }, "payment_modes": { "title": "Formas de pago", @@ -1608,6 +1613,7 @@ "pdf_discount_label": "Descuento", "pdf_amount_label": "Cantidad", "pdf_subtotal": "Subtotal", + "pdf_net_total": "Base Imponible", "pdf_total": "Total", "pdf_payment_label": "Pago", "pdf_payment_receipt_label": "RECIBO DE PAGO", diff --git a/lang/pt-br.json b/lang/pt-br.json index 0017b656..3bb99947 100644 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -217,6 +217,7 @@ "total": "Total", "discount": "Desconto", "sub_total": "Subtotal", + "net_total": "Total Líquido", "estimate_number": "Numero do Orçamento", "ref_number": "Referência", "contact": "Contato", @@ -759,7 +760,11 @@ "updated_message": "Tipo de Imposto Atualizado com sucesso", "deleted_message": "Tipo de Imposto Deletado com sucesso", "confirm_delete": "Você não poderá recuperar este tipo de Imposto", - "already_in_use": "O Imposto já está em uso" + "already_in_use": "O Imposto já está em uso", + "tax_included": "Imposto incluído", + "tax_included_description": "Habilite isso se desejar informar que os Impostos já estão incluídos nos itens da Fatura ou no total da Fatura.", + "tax_included_by_default": "Usar imposto incluído por padrão", + "tax_included_by_default_description": "Habilite isso se desejar definir os impostos incluídos por padrão." }, "expense_category": { "title": "Categoria de Despesa", @@ -932,5 +937,6 @@ "address_maxlength": "O endereço não deve ter mais que 255 caracteres.", "ref_number_maxlength": "O número de referência não deve ter mais que 255 caracteres.", "prefix_maxlength": "O prefixo não deve ter mais que 5 caracteres." - } + }, + "pdf_net_total": "Total Líquido" } diff --git a/lang/pt.json b/lang/pt.json index 95b91aa7..db61e18c 100644 --- a/lang/pt.json +++ b/lang/pt.json @@ -1212,7 +1212,9 @@ "updated_message": "Tipo de Imposto Atualizado com sucesso", "deleted_message": "Tipo de Imposto Deletado com sucesso", "confirm_delete": "Você não poderá recuperar este tipo de Imposto", - "already_in_use": "O Imposto já está em uso" + "already_in_use": "O Imposto já está em uso", + "tax_included": "Imposto incluído", + "tax_included_description": "Habilite isso se desejar informar que os Impostos já estão incluídos nos itens da Fatura ou no total da Fatura." }, "payment_modes": { "title": "Modos de Pagamento", diff --git a/package.json b/package.json index 3f10edc1..b4d8e108 100644 --- a/package.json +++ b/package.json @@ -60,4 +60,4 @@ "vue-router": "^4.5.0", "vuedraggable": "^4.1.0" } -} +} \ No newline at end of file diff --git a/resources/scripts/admin/components/estimate-invoice-common/CreateItemRowTax.vue b/resources/scripts/admin/components/estimate-invoice-common/CreateItemRowTax.vue index 8c7a04e7..d5537b3f 100644 --- a/resources/scripts/admin/components/estimate-invoice-common/CreateItemRowTax.vue +++ b/resources/scripts/admin/components/estimate-invoice-common/CreateItemRowTax.vue @@ -172,6 +172,9 @@ const taxAmount = computed(() => { if (taxPerItemEnabled && !discountPerItemEnabled){ return getTaxAmount() } + if (props.store[props.storeProp].tax_included) { + return Math.round(props.discountedTotal - (props.discountedTotal / (1 + (localTax.percent / 100)))) + } return (props.discountedTotal * localTax.percent) / 100 } return 0 @@ -264,6 +267,7 @@ function getTaxAmount() { const itemTotal = props.discountedTotal const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0 const type = props.store[props.storeProp].discount_type + let discountedTotal = props.discountedTotal if (modelDiscount > 0) { props.store[props.storeProp].items.forEach((_i) => { total += _i.total @@ -271,10 +275,14 @@ function getTaxAmount() { const proportion = (itemTotal / total).toFixed(2) discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100 const itemDiscount = Math.round(discount * proportion) - const discounted = itemTotal - itemDiscount - return Math.round((discounted * localTax.percent) / 100) + discountedTotal = itemTotal - itemDiscount } - return Math.round((props.discountedTotal * localTax.percent) / 100) + + if (props.store[props.storeProp].tax_included) { + return Math.round(discountedTotal - (discountedTotal / (1 + (localTax.percent / 100)))) + } + + return Math.round((discountedTotal * localTax.percent) / 100) } diff --git a/resources/scripts/admin/components/estimate-invoice-common/CreateItems.vue b/resources/scripts/admin/components/estimate-invoice-common/CreateItems.vue index 7d53c4f1..f1e9d0bf 100644 --- a/resources/scripts/admin/components/estimate-invoice-common/CreateItems.vue +++ b/resources/scripts/admin/components/estimate-invoice-common/CreateItems.vue @@ -1,4 +1,27 @@