mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-05-28 06:04:54 +00:00
Merge remote-tracking branch 'origin/master' into frontend-performance-improvements
This commit is contained in:
@@ -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" ]
|
||||
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
2
composer.lock
generated
2
composer.lock
generated
@@ -12363,4 +12363,4 @@
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ return [
|
||||
'exclude' => [
|
||||
base_path('vendor'),
|
||||
base_path('node_modules'),
|
||||
base_path('.git'),
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stateful Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Requests from the following domains / hosts will receive stateful API
|
||||
| authentication cookies. Typically, these should include your local
|
||||
| and production domains which access your API via a frontend SPA.
|
||||
|
|
||||
*/
|
||||
|
||||
'stateful' => 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,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->boolean('tax_included')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('invoices', function (Blueprint $table) {
|
||||
$table->dropColumn('tax_included');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('estimates', function (Blueprint $table) {
|
||||
$table->boolean('tax_included')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('estimates', function (Blueprint $table) {
|
||||
$table->dropColumn('tax_included');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('recurring_invoices', function (Blueprint $table) {
|
||||
$table->boolean('tax_included')->default(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('recurring_invoices', function (Blueprint $table) {
|
||||
$table->dropColumn('tax_included');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -60,4 +60,4 @@
|
||||
"vue-router": "^4.5.0",
|
||||
"vuedraggable": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,27 @@
|
||||
<template>
|
||||
<!-- Tax Included -->
|
||||
<div
|
||||
v-if="companyStore.selectedCompanySettings.tax_included === 'YES'"
|
||||
class="
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
w-full
|
||||
px-6
|
||||
text-base
|
||||
border border-b-0 border-gray-200 border-solid
|
||||
cursor-pointer
|
||||
text-primary-400
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<BaseSwitchSection
|
||||
v-model="taxIncludedField"
|
||||
:title="$t('settings.tax_types.tax_included')"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
/>
|
||||
</div>
|
||||
<table class="text-center item-table min-w-full">
|
||||
<colgroup>
|
||||
<col style="width: 40%; min-width: 280px" />
|
||||
@@ -194,4 +217,14 @@ const defaultCurrency = computed(() => {
|
||||
return companyStore.selectedCompanyCurrency
|
||||
}
|
||||
})
|
||||
|
||||
const taxIncludedField = computed({
|
||||
get: () => {
|
||||
return props.store[props.storeProp].tax_included
|
||||
},
|
||||
set: async (value) => {
|
||||
props.store[props.storeProp].tax_included = value
|
||||
},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
@@ -38,6 +38,17 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="store[storeProp].tax_per_item === 'YES'"
|
||||
>
|
||||
<NetTotal
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
:storeProp="storeProp"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="tax in itemWiseTaxes"
|
||||
:key="tax.tax_type_id"
|
||||
@@ -135,6 +146,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
store[storeProp].tax_per_item === null
|
||||
"
|
||||
class="flex items-center justify-between w-full mt-2"
|
||||
>
|
||||
<NetTotal
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
:storeProp="storeProp"
|
||||
:isLoading="isLoading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
store[storeProp].tax_per_item === 'NO' ||
|
||||
@@ -149,6 +175,7 @@
|
||||
:taxes="taxes"
|
||||
:currency="currency"
|
||||
:store="store"
|
||||
:storeProp="storeProp"
|
||||
@remove="removeTax"
|
||||
@update="updateTax"
|
||||
/>
|
||||
@@ -198,6 +225,7 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref, watch } from 'vue'
|
||||
import Guid from 'guid'
|
||||
import NetTotal from './NetTotal.vue'
|
||||
import Tax from './CreateTotalTaxes.vue'
|
||||
import TaxStub from '@/scripts/admin/stub/abilities'
|
||||
import SelectTaxPopup from './SelectTaxPopup.vue'
|
||||
|
||||
@@ -44,6 +44,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
data: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -66,6 +70,13 @@ const taxAmount = computed(() => {
|
||||
100
|
||||
)
|
||||
}
|
||||
if (props.store.getSubtotalWithDiscount && props.tax.percent && props.store[props.storeProp].tax_included) {
|
||||
return Math.round(
|
||||
props.store.getSubtotalWithDiscount - (
|
||||
props.store.getSubtotalWithDiscount / (1 + (props.tax.percent / 100))
|
||||
)
|
||||
)
|
||||
}
|
||||
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
|
||||
return Math.round(
|
||||
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
|
||||
@@ -83,6 +94,13 @@ watchEffect(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.store[props.storeProp].tax_included,
|
||||
(val) => {
|
||||
updateTax()
|
||||
}, { deep: true },
|
||||
)
|
||||
|
||||
function updateTax() {
|
||||
emit('update', {
|
||||
...props.tax,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="store[storeProp].tax_included"
|
||||
class="flex items-center justify-between w-full"
|
||||
>
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
<label
|
||||
v-else
|
||||
class="text-sm font-semibold leading-5 text-gray-500 uppercase"
|
||||
>
|
||||
{{ $t('estimates.net_total') }}
|
||||
</label>
|
||||
|
||||
<BaseContentPlaceholders v-if="isLoading">
|
||||
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
|
||||
</BaseContentPlaceholders>
|
||||
|
||||
<label
|
||||
v-else
|
||||
class="flex items-center justify-center m-0 text-lg text-black uppercase "
|
||||
>
|
||||
<BaseFormatMoney
|
||||
:amount="store.getNetTotal"
|
||||
:currency="currency"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import BaseContentPlaceholdersText from '@/scripts/components/base/BaseContentPlaceholdersText.vue'
|
||||
import BaseContentPlaceholders from '@/scripts/components/base/BaseContentPlaceholders.vue'
|
||||
import BaseFormatMoney from '@/scripts/components/base/BaseFormatMoney.vue'
|
||||
|
||||
const props = defineProps({
|
||||
store: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
storeProp: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currency: {
|
||||
type: [Object, String],
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
71
resources/scripts/admin/stores/estimate.js
vendored
71
resources/scripts/admin/stores/estimate.js
vendored
@@ -44,6 +44,9 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
return a + b['total']
|
||||
}, 0)
|
||||
},
|
||||
getNetTotal() {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
},
|
||||
getTotalSimpleTax() {
|
||||
return _.sumBy(this.newEstimate.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
@@ -79,6 +82,9 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
},
|
||||
|
||||
getTotal() {
|
||||
if (this.newEstimate.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
|
||||
@@ -149,7 +155,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.log(err)
|
||||
handleError(err)
|
||||
reject(err)
|
||||
})
|
||||
@@ -160,20 +166,19 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
Object.assign(this.newEstimate, estimate)
|
||||
if (this.newEstimate.tax_per_item === 'YES') {
|
||||
this.newEstimate.items.forEach((_i) => {
|
||||
if (_i.taxes && !_i.taxes.length){
|
||||
if (_i.taxes && !_i.taxes.length) {
|
||||
_i.taxes.push({ ...taxStub, id: Guid.raw() })
|
||||
}
|
||||
})
|
||||
}
|
||||
if (this.newEstimate.discount_per_item === 'YES') {
|
||||
this.newEstimate.items.forEach((_i, index) => {
|
||||
if (_i.discount_type === 'fixed'){
|
||||
if (_i.discount_type === 'fixed') {
|
||||
this.newEstimate.items[index].discount = _i.discount / 100
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
if (this.newEstimate.discount_type === 'fixed'){
|
||||
} else {
|
||||
if (this.newEstimate.discount_type === 'fixed') {
|
||||
this.newEstimate.discount = this.newEstimate.discount / 100
|
||||
}
|
||||
}
|
||||
@@ -182,19 +187,23 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
setCustomerAddresses(customer) {
|
||||
const customer_business = customer.customer_business
|
||||
|
||||
if (customer_business?.billing_address){
|
||||
this.newEstimate.customer.billing_address = customer_business.billing_address
|
||||
if (customer_business?.billing_address) {
|
||||
this.newEstimate.customer.billing_address =
|
||||
customer_business.billing_address
|
||||
}
|
||||
|
||||
if (customer_business?.shipping_address){
|
||||
this.newEstimate.customer.shipping_address = customer_business.shipping_address
|
||||
if (customer_business?.shipping_address) {
|
||||
this.newEstimate.customer.shipping_address =
|
||||
customer_business.shipping_address
|
||||
}
|
||||
},
|
||||
|
||||
addSalesTaxUs() {
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
let salesTax = { ...taxStub }
|
||||
let found = this.newEstimate.taxes.find((_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE')
|
||||
let found = this.newEstimate.taxes.find(
|
||||
(_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE',
|
||||
)
|
||||
if (found) {
|
||||
for (const key in found) {
|
||||
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
|
||||
@@ -202,10 +211,10 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
}
|
||||
}
|
||||
salesTax.id = found.tax_type_id
|
||||
console.log(salesTax, 'salesTax');
|
||||
console.log(salesTax, 'salesTax')
|
||||
|
||||
taxTypeStore.taxTypes.push(salesTax)
|
||||
console.log(taxTypeStore.taxTypes);
|
||||
console.log(taxTypeStore.taxTypes)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -261,7 +270,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
.post(`/api/v1/estimates/delete`, id)
|
||||
.then((response) => {
|
||||
let index = this.estimates.findIndex(
|
||||
(estimate) => estimate.id === id
|
||||
(estimate) => estimate.id === id,
|
||||
)
|
||||
|
||||
this.estimates.splice(index, 1)
|
||||
@@ -288,7 +297,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
.then((response) => {
|
||||
this.selectedEstimates.forEach((estimate) => {
|
||||
let index = this.estimates.findIndex(
|
||||
(_est) => _est.id === estimate.id
|
||||
(_est) => _est.id === estimate.id,
|
||||
)
|
||||
this.estimates.splice(index, 1)
|
||||
})
|
||||
@@ -313,7 +322,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
.put(`/api/v1/estimates/${data.id}`, data)
|
||||
.then((response) => {
|
||||
let pos = this.estimates.findIndex(
|
||||
(estimate) => estimate.id === response.data.data.id
|
||||
(estimate) => estimate.id === response.data.data.id,
|
||||
)
|
||||
this.estimates[pos] = response.data.data
|
||||
const notificationStore = useNotificationStore()
|
||||
@@ -355,7 +364,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
.post(`/api/v1/estimates/${data.id}/status`, data)
|
||||
.then((response) => {
|
||||
let pos = this.estimates.findIndex(
|
||||
(estimate) => estimate.id === data.id
|
||||
(estimate) => estimate.id === data.id,
|
||||
)
|
||||
if (this.estimates[pos]) {
|
||||
this.estimates[pos].status = 'ACCEPTED'
|
||||
@@ -402,7 +411,7 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
.post(`/api/v1/estimates/${data.id}/status`, data)
|
||||
.then((response) => {
|
||||
let pos = this.estimates.findIndex(
|
||||
(estimate) => estimate.id === data.id
|
||||
(estimate) => estimate.id === data.id,
|
||||
)
|
||||
if (this.estimates[pos]) {
|
||||
this.estimates[pos].status = 'SENT'
|
||||
@@ -570,17 +579,26 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
|
||||
if (!isEdit) {
|
||||
await notesStore.fetchNotes()
|
||||
this.newEstimate.notes = notesStore.getDefaultNoteForType('Estimate')?.notes
|
||||
this.newEstimate.notes =
|
||||
notesStore.getDefaultNoteForType('Estimate')?.notes
|
||||
this.newEstimate.tax_per_item =
|
||||
companyStore.selectedCompanySettings.tax_per_item
|
||||
this.newEstimate.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newEstimate.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newEstimate.sales_tax_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newEstimate.sales_tax_address_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newEstimate.discount_per_item =
|
||||
companyStore.selectedCompanySettings.discount_per_item
|
||||
this.newEstimate.estimate_date = moment().format('YYYY-MM-DD')
|
||||
if (companyStore.selectedCompanySettings.estimate_set_expiry_date_automatically === 'YES') {
|
||||
if (
|
||||
companyStore.selectedCompanySettings
|
||||
.estimate_set_expiry_date_automatically === 'YES'
|
||||
) {
|
||||
this.newEstimate.expiry_date = moment()
|
||||
.add(companyStore.selectedCompanySettings.estimate_expiry_date_days, 'days')
|
||||
.add(
|
||||
companyStore.selectedCompanySettings.estimate_expiry_date_days,
|
||||
'days',
|
||||
)
|
||||
.format('YYYY-MM-DD')
|
||||
}
|
||||
} else {
|
||||
@@ -607,9 +625,10 @@ export const useEstimateStore = (useWindow = false) => {
|
||||
}
|
||||
|
||||
this.setTemplate(this.templates[0].name)
|
||||
this.newEstimate.template_name =
|
||||
userStore.currentUserSettings.default_estimate_template ?
|
||||
userStore.currentUserSettings.default_estimate_template : this.newEstimate.template_name
|
||||
this.newEstimate.template_name = userStore.currentUserSettings
|
||||
.default_estimate_template
|
||||
? userStore.currentUserSettings.default_estimate_template
|
||||
: this.newEstimate.template_name
|
||||
}
|
||||
|
||||
if (isEdit) {
|
||||
|
||||
@@ -185,7 +185,7 @@ export const useInstallationStore = (useWindow = false) => {
|
||||
})
|
||||
},
|
||||
|
||||
checkAutheticated() {
|
||||
checkAuthenticated() {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`/api/v1/auth/check`)
|
||||
|
||||
58
resources/scripts/admin/stores/invoice.js
vendored
58
resources/scripts/admin/stores/invoice.js
vendored
@@ -51,6 +51,10 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
}, 0)
|
||||
},
|
||||
|
||||
getNetTotal() {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
},
|
||||
|
||||
getTotalSimpleTax() {
|
||||
return _.sumBy(this.newInvoice.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
@@ -86,6 +90,9 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
},
|
||||
|
||||
getTotal() {
|
||||
if (this.newInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
|
||||
@@ -160,8 +167,7 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
if (_i.discount_type === 'fixed')
|
||||
this.newInvoice.items[index].discount = _i.discount / 100
|
||||
})
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (this.newInvoice.discount_type === 'fixed')
|
||||
this.newInvoice.discount = this.newInvoice.discount / 100
|
||||
}
|
||||
@@ -171,16 +177,20 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
const customer_business = customer.customer_business
|
||||
|
||||
if (customer_business?.billing_address)
|
||||
this.newInvoice.customer.billing_address = customer_business.billing_address
|
||||
this.newInvoice.customer.billing_address =
|
||||
customer_business.billing_address
|
||||
|
||||
if (customer_business?.shipping_address)
|
||||
this.newInvoice.customer.shipping_address = customer_business.shipping_address
|
||||
this.newInvoice.customer.shipping_address =
|
||||
customer_business.shipping_address
|
||||
},
|
||||
|
||||
addSalesTaxUs() {
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
let salesTax = { ...taxStub }
|
||||
let found = this.newInvoice.taxes.find((_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE')
|
||||
let found = this.newInvoice.taxes.find(
|
||||
(_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE',
|
||||
)
|
||||
if (found) {
|
||||
for (const key in found) {
|
||||
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
|
||||
@@ -237,7 +247,7 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
.post(`/api/v1/invoices/delete`, id)
|
||||
.then((response) => {
|
||||
let index = this.invoices.findIndex(
|
||||
(invoice) => invoice.id === id
|
||||
(invoice) => invoice.id === id,
|
||||
)
|
||||
this.invoices.splice(index, 1)
|
||||
|
||||
@@ -261,7 +271,7 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
.then((response) => {
|
||||
this.selectedInvoices.forEach((invoice) => {
|
||||
let index = this.invoices.findIndex(
|
||||
(_inv) => _inv.id === invoice.id
|
||||
(_inv) => _inv.id === invoice.id,
|
||||
)
|
||||
this.invoices.splice(index, 1)
|
||||
})
|
||||
@@ -286,7 +296,7 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
.put(`/api/v1/invoices/${data.id}`, data)
|
||||
.then((response) => {
|
||||
let pos = this.invoices.findIndex(
|
||||
(invoice) => invoice.id === response.data.data.id
|
||||
(invoice) => invoice.id === response.data.data.id,
|
||||
)
|
||||
this.invoices[pos] = response.data.data
|
||||
|
||||
@@ -328,7 +338,7 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
.post(`/api/v1/invoices/${data.id}/status`, data)
|
||||
.then((response) => {
|
||||
let pos = this.invoices.findIndex(
|
||||
(invoices) => invoices.id === data.id
|
||||
(invoices) => invoices.id === data.id,
|
||||
)
|
||||
|
||||
if (this.invoices[pos]) {
|
||||
@@ -493,26 +503,35 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
}
|
||||
|
||||
let editActions = []
|
||||
|
||||
|
||||
if (!isEdit) {
|
||||
await notesStore.fetchNotes()
|
||||
this.newInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes
|
||||
this.newInvoice.notes =
|
||||
notesStore.getDefaultNoteForType('Invoice')?.notes
|
||||
this.newInvoice.tax_per_item =
|
||||
companyStore.selectedCompanySettings.tax_per_item
|
||||
this.newInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newInvoice.sales_tax_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newInvoice.sales_tax_address_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newInvoice.discount_per_item =
|
||||
companyStore.selectedCompanySettings.discount_per_item
|
||||
|
||||
let dateFormat = 'YYYY-MM-DD';
|
||||
let dateFormat = 'YYYY-MM-DD'
|
||||
if (companyStore.selectedCompanySettings.invoice_use_time === 'YES') {
|
||||
dateFormat += ' HH:mm'
|
||||
}
|
||||
|
||||
this.newInvoice.invoice_date = moment().format(dateFormat)
|
||||
if (companyStore.selectedCompanySettings.invoice_set_due_date_automatically === 'YES') {
|
||||
if (
|
||||
companyStore.selectedCompanySettings
|
||||
.invoice_set_due_date_automatically === 'YES'
|
||||
) {
|
||||
this.newInvoice.due_date = moment()
|
||||
.add(companyStore.selectedCompanySettings.invoice_due_date_days, 'days')
|
||||
.add(
|
||||
companyStore.selectedCompanySettings.invoice_due_date_days,
|
||||
'days',
|
||||
)
|
||||
.format('YYYY-MM-DD')
|
||||
}
|
||||
} else {
|
||||
@@ -539,9 +558,10 @@ export const useInvoiceStore = (useWindow = false) => {
|
||||
|
||||
if (res3.data) {
|
||||
this.setTemplate(this.templates[0].name)
|
||||
this.newInvoice.template_name =
|
||||
userStore.currentUserSettings.default_invoice_template ?
|
||||
userStore.currentUserSettings.default_invoice_template : this.newInvoice.template_name
|
||||
this.newInvoice.template_name = userStore.currentUserSettings
|
||||
.default_invoice_template
|
||||
? userStore.currentUserSettings.default_invoice_template
|
||||
: this.newInvoice.template_name
|
||||
}
|
||||
}
|
||||
if (isEdit) {
|
||||
|
||||
108
resources/scripts/admin/stores/recurring-invoice.js
vendored
108
resources/scripts/admin/stores/recurring-invoice.js
vendored
@@ -37,17 +37,56 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
},
|
||||
|
||||
frequencies: [
|
||||
{ label: global.t('recurring_invoices.frequency.every_minute'), value: '* * * * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_30_minute'), value: '*/30 * * * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_hour'), value: '0 * * * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_2_hour'), value: '0 */2 * * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_day_at_midnight'), value: '0 0 * * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_week'), value: '0 0 * * 0' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_15_days_at_midnight'), value: '0 5 */15 * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.on_the_first_day_of_every_month_at_midnight'), value: '0 0 1 * *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_6_month'), value: '0 0 1 */6 *' },
|
||||
{ label: global.t('recurring_invoices.frequency.every_year_on_the_first_day_of_january_at_midnight'), value: '0 0 1 1 *' },
|
||||
{ label: global.t('recurring_invoices.frequency.custom'), value: 'CUSTOM' },
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_minute'),
|
||||
value: '* * * * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_30_minute'),
|
||||
value: '*/30 * * * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_hour'),
|
||||
value: '0 * * * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_2_hour'),
|
||||
value: '0 */2 * * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_day_at_midnight'),
|
||||
value: '0 0 * * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_week'),
|
||||
value: '0 0 * * 0',
|
||||
},
|
||||
{
|
||||
label: global.t(
|
||||
'recurring_invoices.frequency.every_15_days_at_midnight',
|
||||
),
|
||||
value: '0 5 */15 * *',
|
||||
},
|
||||
{
|
||||
label: global.t(
|
||||
'recurring_invoices.frequency.on_the_first_day_of_every_month_at_midnight',
|
||||
),
|
||||
value: '0 0 1 * *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.every_6_month'),
|
||||
value: '0 0 1 */6 *',
|
||||
},
|
||||
{
|
||||
label: global.t(
|
||||
'recurring_invoices.frequency.every_year_on_the_first_day_of_january_at_midnight',
|
||||
),
|
||||
value: '0 0 1 1 *',
|
||||
},
|
||||
{
|
||||
label: global.t('recurring_invoices.frequency.custom'),
|
||||
value: 'CUSTOM',
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -60,6 +99,10 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
)
|
||||
},
|
||||
|
||||
getNetTotal() {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
},
|
||||
|
||||
getTotalSimpleTax() {
|
||||
return _.sumBy(this.newRecurringInvoice.taxes, function (tax) {
|
||||
if (!tax.compound_tax) {
|
||||
@@ -95,6 +138,9 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
},
|
||||
|
||||
getTotal() {
|
||||
if (this.newRecurringInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
},
|
||||
@@ -174,7 +220,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
})
|
||||
|
||||
let pos = this.recurringInvoices.findIndex(
|
||||
(invoice) => invoice.id === response.data.data.id
|
||||
(invoice) => invoice.id === response.data.data.id,
|
||||
)
|
||||
|
||||
this.recurringInvoices[pos] = response.data.data
|
||||
@@ -240,7 +286,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
.post(`/api/v1/recurring-invoices/delete`, id)
|
||||
.then((response) => {
|
||||
let index = this.recurringInvoices.findIndex(
|
||||
(invoice) => invoice.id === id
|
||||
(invoice) => invoice.id === id,
|
||||
)
|
||||
this.recurringInvoices.splice(index, 1)
|
||||
resolve(response)
|
||||
@@ -265,7 +311,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
.then((response) => {
|
||||
this.selectedRecurringInvoices.forEach((invoice) => {
|
||||
let index = this.recurringInvoices.findIndex(
|
||||
(_inv) => _inv.id === invoice.id
|
||||
(_inv) => _inv.id === invoice.id,
|
||||
)
|
||||
this.recurringInvoices.splice(index, 1)
|
||||
})
|
||||
@@ -305,7 +351,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
this.selectAllField = false
|
||||
} else {
|
||||
let allInvoiceIds = this.recurringInvoices.map(
|
||||
(invoice) => invoice.id
|
||||
(invoice) => invoice.id,
|
||||
)
|
||||
this.selectedRecurringInvoices = allInvoiceIds
|
||||
this.selectAllField = true
|
||||
@@ -351,13 +397,16 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
// on create
|
||||
if (!isEdit) {
|
||||
await notesStore.fetchNotes()
|
||||
this.newRecurringInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes
|
||||
this.newRecurringInvoice.notes =
|
||||
notesStore.getDefaultNoteForType('Invoice')?.notes
|
||||
this.newRecurringInvoice.tax_per_item =
|
||||
companyStore.selectedCompanySettings.tax_per_item
|
||||
this.newRecurringInvoice.discount_per_item =
|
||||
companyStore.selectedCompanySettings.discount_per_item
|
||||
this.newRecurringInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newRecurringInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newRecurringInvoice.sales_tax_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_type
|
||||
this.newRecurringInvoice.sales_tax_address_type =
|
||||
companyStore.selectedCompanySettings.sales_tax_address_type
|
||||
this.newRecurringInvoice.starts_at = moment().format('YYYY-MM-DD')
|
||||
this.newRecurringInvoice.next_invoice_date = moment()
|
||||
.add(7, 'days')
|
||||
@@ -399,7 +448,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
this.isFetchingInitialSettings = false
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
console.log(err)
|
||||
handleError(err)
|
||||
})
|
||||
},
|
||||
@@ -407,7 +456,9 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
addSalesTaxUs() {
|
||||
const taxTypeStore = useTaxTypeStore()
|
||||
let salesTax = { ...TaxStub }
|
||||
let found = this.newRecurringInvoice.taxes.find((_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE')
|
||||
let found = this.newRecurringInvoice.taxes.find(
|
||||
(_t) => _t.name === 'Sales Tax' && _t.type === 'MODULE',
|
||||
)
|
||||
if (found) {
|
||||
for (const key in found) {
|
||||
if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
|
||||
@@ -424,14 +475,15 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
|
||||
},
|
||||
|
||||
setSelectedFrequency() {
|
||||
let data = this.frequencies.find(
|
||||
(frequency) => {
|
||||
return frequency.value === this.newRecurringInvoice.frequency
|
||||
}
|
||||
)
|
||||
data ? this.newRecurringInvoice.selectedFrequency = data
|
||||
: this.newRecurringInvoice.selectedFrequency = { label: 'Custom', value: 'CUSTOM' }
|
||||
|
||||
let data = this.frequencies.find((frequency) => {
|
||||
return frequency.value === this.newRecurringInvoice.frequency
|
||||
})
|
||||
data
|
||||
? (this.newRecurringInvoice.selectedFrequency = data)
|
||||
: (this.newRecurringInvoice.selectedFrequency = {
|
||||
label: 'Custom',
|
||||
value: 'CUSTOM',
|
||||
})
|
||||
},
|
||||
|
||||
resetSelectedNote() {
|
||||
|
||||
1
resources/scripts/admin/stub/estimate.js
vendored
1
resources/scripts/admin/stub/estimate.js
vendored
@@ -8,6 +8,7 @@ export default function () {
|
||||
customer: null,
|
||||
template_name: '',
|
||||
tax_per_item: null,
|
||||
tax_included: false,
|
||||
sales_tax_type: null,
|
||||
sales_tax_address_type: null,
|
||||
discount_per_item: null,
|
||||
|
||||
1
resources/scripts/admin/stub/invoice.js
vendored
1
resources/scripts/admin/stub/invoice.js
vendored
@@ -20,6 +20,7 @@ export default function () {
|
||||
sub_total: 0,
|
||||
total: 0,
|
||||
tax_per_item: null,
|
||||
tax_included: false,
|
||||
sales_tax_type: null,
|
||||
sales_tax_address_type: null,
|
||||
discount_per_item: null,
|
||||
|
||||
@@ -250,6 +250,14 @@ customFieldStore.resetCustomFields()
|
||||
v$.value.$reset
|
||||
estimateStore.fetchEstimateInitialSettings(isEdit.value)
|
||||
|
||||
watch(
|
||||
() => companyStore.selectedCompanySettings?.tax_included_by_default,
|
||||
(newVal) => {
|
||||
estimateStore.newEstimate.tax_included = newVal === 'YES'
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ async function verifyDomain() {
|
||||
try {
|
||||
await installationStore.setInstallationDomain(formData)
|
||||
await installationStore.installationLogin()
|
||||
let driverRes = await installationStore.checkAutheticated()
|
||||
let driverRes = await installationStore.checkAuthenticated()
|
||||
|
||||
if (driverRes.data) {
|
||||
emit('next', 4)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="next">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.database.app_url')"
|
||||
:error="v$.app_url.$error && v$.app_url.$errors[0].$message"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="next">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.database.app_url')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<form action="" @submit.prevent="next">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 lg:mb-6 md:mb-6">
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2 mb-6">
|
||||
<BaseInputGroup
|
||||
:label="$t('wizard.database.app_url')"
|
||||
:content-loading="isFetchingInitialData"
|
||||
|
||||
@@ -404,8 +404,8 @@ onSearched = debounce(onSearched, 500)
|
||||
</BaseDropdown>
|
||||
|
||||
<BaseButton class="ml-1" size="md" variant="gray" @click="sortData">
|
||||
<BaseIcon v-if="getOrderBy" name="SortAscendingIcon" />
|
||||
<BaseIcon v-else name="SortDescendingIcon" />
|
||||
<BaseIcon v-if="getOrderBy" name="BarsArrowUpIcon" />
|
||||
<BaseIcon v-else name="BarsArrowDownIcon" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -254,6 +254,14 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => companyStore.selectedCompanySettings?.tax_included_by_default,
|
||||
(newVal) => {
|
||||
invoiceStore.newInvoice.tax_included = newVal === 'YES'
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
|
||||
@@ -191,7 +191,7 @@ const taxes = computed({
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: `${tax.name} (${tax.calculation_type === 'fixed'
|
||||
tax_name: `${tax.name} (${tax.calculation_type === 'fixed'
|
||||
? new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: companyStore.selectedCompanyCurrency.code
|
||||
@@ -216,7 +216,7 @@ const getTaxTypes = computed(() => {
|
||||
return {
|
||||
...tax,
|
||||
tax_type_id: tax.id,
|
||||
tax_name: `${tax.name} (${tax.calculation_type === 'fixed'
|
||||
tax_name: `${tax.name} (${tax.calculation_type === 'fixed'
|
||||
? new Intl.NumberFormat(undefined, {
|
||||
style: 'currency',
|
||||
currency: companyStore.selectedCompanyCurrency.code
|
||||
@@ -299,7 +299,7 @@ async function submitItem() {
|
||||
tax_type_id: tax.tax_type_id,
|
||||
calculation_type: tax.calculation_type,
|
||||
fixed_amount: tax.fixed_amount,
|
||||
amount: tax.calculation_type === 'fixed' ? tax.fixed_amount : price.value * tax.percent,
|
||||
amount: tax.calculation_type === 'fixed' ? tax.fixed_amount : Math.round(price.value * tax.percent),
|
||||
percent: tax.percent,
|
||||
name: tax.name,
|
||||
collective_tax: 0,
|
||||
|
||||
@@ -284,6 +284,14 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => companyStore.selectedCompanySettings?.tax_included_by_default,
|
||||
(newVal) => {
|
||||
recurringInvoiceStore.newRecurringInvoice.tax_included = newVal === 'YES'
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
async function submitForm() {
|
||||
v$.value.$touch()
|
||||
|
||||
|
||||
@@ -52,6 +52,21 @@
|
||||
:title="$t('settings.tax_types.tax_per_item')"
|
||||
:description="$t('settings.tax_types.tax_setting_description')"
|
||||
/>
|
||||
|
||||
<BaseDivider class="mt-8 mb-2" />
|
||||
|
||||
<BaseSwitchSection
|
||||
v-model="taxIncludedField"
|
||||
:title="$t('settings.tax_types.tax_included')"
|
||||
:description="$t('settings.tax_types.tax_included_description')"
|
||||
/>
|
||||
|
||||
<BaseSwitchSection
|
||||
v-if="taxIncludedField"
|
||||
v-model="taxIncludedByDefaultField"
|
||||
:title="$t('settings.tax_types.tax_included_by_default')"
|
||||
:description="$t('settings.tax_types.tax_included_by_default_description')"
|
||||
/>
|
||||
</div>
|
||||
</BaseSettingCard>
|
||||
</template>
|
||||
@@ -145,6 +160,61 @@ const taxPerItemField = computed({
|
||||
},
|
||||
})
|
||||
|
||||
const taxIncludedSettings = reactive({
|
||||
tax_included: 'NO',
|
||||
tax_included_by_default: 'NO',
|
||||
})
|
||||
|
||||
utils.mergeSettings(taxIncludedSettings, {
|
||||
...companyStore.selectedCompanySettings,
|
||||
})
|
||||
|
||||
const taxIncludedField = computed({
|
||||
get: () => {
|
||||
return taxIncludedSettings.tax_included === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
taxIncludedSettings.tax_included = value
|
||||
|
||||
if (!newValue) {
|
||||
taxIncludedSettings.tax_included_by_default = 'NO'
|
||||
}
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
...taxIncludedSettings,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const taxIncludedByDefaultField = computed({
|
||||
get: () => {
|
||||
return taxIncludedSettings.tax_included_by_default === 'YES'
|
||||
},
|
||||
set: async (newValue) => {
|
||||
const value = newValue ? 'YES' : 'NO'
|
||||
taxIncludedSettings.tax_included_by_default = value
|
||||
|
||||
let data = {
|
||||
settings: {
|
||||
tax_included_by_default: taxIncludedSettings.tax_included_by_default,
|
||||
},
|
||||
}
|
||||
|
||||
await companyStore.updateCompanySettings({
|
||||
data,
|
||||
message: 'general.setting_updated',
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function hasAtleastOneAbility() {
|
||||
return userStore.hasAbilities([
|
||||
abilities.DELETE_TAX_TYPE,
|
||||
|
||||
4
resources/scripts/plugins/axios.js
vendored
4
resources/scripts/plugins/axios.js
vendored
@@ -20,11 +20,11 @@ axios.interceptors.request.use(function (config) {
|
||||
const authToken = Ls.get('auth.token')
|
||||
|
||||
if (authToken) {
|
||||
config.headers.common.Authorization = authToken
|
||||
config.headers.Authorization = authToken
|
||||
}
|
||||
|
||||
if (companyId) {
|
||||
config.headers.common['company'] = companyId
|
||||
config.headers.company = companyId
|
||||
}
|
||||
|
||||
return config
|
||||
|
||||
@@ -101,7 +101,18 @@
|
||||
</tr>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
|
||||
@if ($estimate->tax_included)
|
||||
<tr>
|
||||
<td class="border-0 total-table-attribute-label">
|
||||
@lang('pdf_net_total')
|
||||
</td>
|
||||
<td class="py-2 border-0 item-cell total-table-attribute-value">
|
||||
{!! format_money_pdf($estimate->sub_total - $estimate->discount - $estimate->tax, $estimate->customer->currency) !!}
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if ($estimate->tax_per_item === 'YES')
|
||||
@foreach ($taxes as $tax)
|
||||
<tr>
|
||||
@@ -133,7 +144,7 @@
|
||||
</tr>
|
||||
@endforeach
|
||||
@endif
|
||||
|
||||
|
||||
<tr>
|
||||
<td class="py-3"></td>
|
||||
<td class="py-3"></td>
|
||||
|
||||
@@ -121,6 +121,17 @@
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@if ($invoice->tax_included)
|
||||
<tr>
|
||||
<td class="border-0 total-table-attribute-label">
|
||||
@lang('pdf_net_total')
|
||||
</td>
|
||||
<td class="py-2 border-0 item-cell total-table-attribute-value">
|
||||
{!! format_money_pdf($invoice->sub_total - $invoice->discount - $invoice->tax, $invoice->customer->currency) !!}
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
|
||||
@if ($invoice->tax_per_item === 'YES')
|
||||
@foreach ($taxes as $tax)
|
||||
<tr>
|
||||
|
||||
@@ -113,7 +113,8 @@ Route::prefix('/customer')->group(function () {
|
||||
|
||||
Route::get('/installation', function () {
|
||||
return view('app');
|
||||
})->name('install')->middleware('redirect-if-installed');
|
||||
})->name('install')
|
||||
->middleware(['redirect-if-installed']);
|
||||
|
||||
// Move other http requests to the Vue App
|
||||
// -------------------------------------------------
|
||||
|
||||
@@ -105,6 +105,8 @@ test('update settings', function () {
|
||||
'notify_invoice_viewed' => 'YES',
|
||||
'notify_estimate_viewed' => 'YES',
|
||||
'tax_per_item' => 'YES',
|
||||
'tax_included' => 'YES',
|
||||
'tax_included_by_default' => 'YES',
|
||||
'discount_per_item' => 'YES',
|
||||
];
|
||||
|
||||
|
||||
@@ -463,3 +463,23 @@ test('update estimate with EUR currency', function () {
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('create estimate with tax included', function () {
|
||||
$estimate = Estimate::factory()->raw([
|
||||
'estimate_number' => 'EST-000006',
|
||||
'items' => [
|
||||
EstimateItem::factory()->raw(),
|
||||
],
|
||||
'taxes' => [
|
||||
Tax::factory()->raw(),
|
||||
],
|
||||
'tax_included' => true,
|
||||
]);
|
||||
|
||||
postJson('api/v1/estimates', $estimate)
|
||||
->assertStatus(201);
|
||||
|
||||
$this->assertDatabaseHas('estimates', [
|
||||
'tax_included' => $estimate['tax_included'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -520,3 +520,20 @@ test('update invoice with EUR currency', function () {
|
||||
'base_amount' => $invoice2['taxes'][0]['base_amount'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('create invoice with tax included', function () {
|
||||
$invoice = Invoice::factory()
|
||||
->raw([
|
||||
'taxes' => [Tax::factory()->raw()],
|
||||
'items' => [InvoiceItem::factory()->raw()],
|
||||
'tax_included' => true,
|
||||
]);
|
||||
|
||||
$response = postJson('api/v1/invoices', $invoice);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('invoices', [
|
||||
'tax_included' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user