feat: Tax included (#370)

* feat: Tax included

* Added a toggle switch in tax settings to enable the feature.
* Database migration adding tax_included field into estimates, invoices
  and recurring invoices table.
* Toggle switch to enable and store the tax_included by estimates,
  invoices and recurring invoices.
* In case of tax included enabled, total taxes will be recalculated and
  the invoices, estimates and recurring invoices total won't be sum with
  taxes.
* Apply tax included when discount_per_item/tax_per_item item is enabled.
* Custom component to show the net total when tax included is enabled.
* Update invoice and estimates pdfs with net total.

* chore: Tax included by default

A switch button inside the tax settings to enable the tax included by
default in invoices, estimates and recurring invoices.
This commit is contained in:
Fabio Ribeiro
2025-08-28 10:28:24 +02:00
committed by GitHub
parent 08e1bb2e22
commit d69a56e2d5
32 changed files with 582 additions and 83 deletions

View File

@@ -21,6 +21,7 @@ class EstimateResource extends JsonResource
'status' => $this->status, 'status' => $this->status,
'reference_number' => $this->reference_number, 'reference_number' => $this->reference_number,
'tax_per_item' => $this->tax_per_item, 'tax_per_item' => $this->tax_per_item,
'tax_included' => $this->tax_included,
'discount_per_item' => $this->discount_per_item, 'discount_per_item' => $this->discount_per_item,
'notes' => $this->getNotes(), 'notes' => $this->getNotes(),
'discount' => $this->discount, 'discount' => $this->discount,

View File

@@ -22,6 +22,7 @@ class InvoiceResource extends JsonResource
'status' => $this->status, 'status' => $this->status,
'paid_status' => $this->paid_status, 'paid_status' => $this->paid_status,
'tax_per_item' => $this->tax_per_item, 'tax_per_item' => $this->tax_per_item,
'tax_included' => $this->tax_included,
'discount_per_item' => $this->discount_per_item, 'discount_per_item' => $this->discount_per_item,
'notes' => $this->notes, 'notes' => $this->notes,
'discount_type' => $this->discount_type, 'discount_type' => $this->discount_type,

View File

@@ -32,6 +32,7 @@ class RecurringInvoiceResource extends JsonResource
'limit_date' => $this->limit_date, 'limit_date' => $this->limit_date,
'exchange_rate' => $this->exchange_rate, 'exchange_rate' => $this->exchange_rate,
'tax_per_item' => $this->tax_per_item, 'tax_per_item' => $this->tax_per_item,
'tax_included' => $this->tax_included,
'discount_per_item' => $this->discount_per_item, 'discount_per_item' => $this->discount_per_item,
'notes' => $this->notes, 'notes' => $this->notes,
'discount_type' => $this->discount_type, 'discount_type' => $this->discount_type,

View File

@@ -93,6 +93,7 @@ class EstimateFactory extends Factory
return $estimate['discount_type'] == 'percentage' ? (($estimate['discount_val'] * $estimate['total']) / 100) : $estimate['discount_val']; return $estimate['discount_type'] == 'percentage' ? (($estimate['discount_val'] * $estimate['total']) / 100) : $estimate['discount_val'];
}, },
'tax_per_item' => 'YES', 'tax_per_item' => 'YES',
'tax_included' => false,
'discount_per_item' => 'No', 'discount_per_item' => 'No',
'tax' => $this->faker->randomDigitNotNull(), 'tax' => $this->faker->randomDigitNotNull(),
'notes' => $this->faker->text(80), 'notes' => $this->faker->text(80),

View File

@@ -93,6 +93,7 @@ class InvoiceFactory extends Factory
'template_name' => 'invoice1', 'template_name' => 'invoice1',
'status' => Invoice::STATUS_DRAFT, 'status' => Invoice::STATUS_DRAFT,
'tax_per_item' => 'NO', 'tax_per_item' => 'NO',
'tax_included' => false,
'discount_per_item' => 'NO', 'discount_per_item' => 'NO',
'paid_status' => Invoice::STATUS_UNPAID, 'paid_status' => Invoice::STATUS_UNPAID,
'company_id' => User::find(1)->companies()->first()->id, 'company_id' => User::find(1)->companies()->first()->id,

View File

@@ -26,6 +26,7 @@ class RecurringInvoiceFactory extends Factory
'send_automatically' => false, 'send_automatically' => false,
'status' => $this->faker->randomElement(['COMPLETED', 'ON_HOLD', 'ACTIVE']), 'status' => $this->faker->randomElement(['COMPLETED', 'ON_HOLD', 'ACTIVE']),
'tax_per_item' => 'NO', 'tax_per_item' => 'NO',
'tax_included' => false,
'discount_per_item' => 'NO', 'discount_per_item' => 'NO',
'sub_total' => $this->faker->randomDigitNotNull(), 'sub_total' => $this->faker->randomDigitNotNull(),
'total' => $this->faker->randomDigitNotNull(), 'total' => $this->faker->randomDigitNotNull(),

View File

@@ -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');
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -306,6 +306,7 @@
"total": "Total", "total": "Total",
"discount": "Discount", "discount": "Discount",
"sub_total": "Sub Total", "sub_total": "Sub Total",
"net_total": "Net",
"estimate_number": "Estimate Number", "estimate_number": "Estimate Number",
"ref_number": "Ref Number", "ref_number": "Ref Number",
"contact": "Contact", "contact": "Contact",
@@ -1220,7 +1221,11 @@
"updated_message": "Tax type updated successfully", "updated_message": "Tax type updated successfully",
"deleted_message": "Tax type deleted successfully", "deleted_message": "Tax type deleted successfully",
"confirm_delete": "You will not be able to recover this Tax Type", "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": { "payment_modes": {
"title": "Payment Modes", "title": "Payment Modes",
@@ -1616,6 +1621,7 @@
"pdf_discount_label": "Discount", "pdf_discount_label": "Discount",
"pdf_amount_label": "Amount", "pdf_amount_label": "Amount",
"pdf_subtotal": "Subtotal", "pdf_subtotal": "Subtotal",
"pdf_net_total": "Net",
"pdf_total": "Total", "pdf_total": "Total",
"pdf_payment_label": "Payment", "pdf_payment_label": "Payment",
"pdf_payment_receipt_label": "PAYMENT RECEIPT", "pdf_payment_receipt_label": "PAYMENT RECEIPT",

View File

@@ -303,6 +303,7 @@
"total": "Total", "total": "Total",
"discount": "Descuento", "discount": "Descuento",
"sub_total": "Subtotal", "sub_total": "Subtotal",
"net_total": "Base Imponible",
"estimate_number": "Número de Presupuesto", "estimate_number": "Número de Presupuesto",
"ref_number": "Número de referencia", "ref_number": "Número de referencia",
"contact": "Contacto", "contact": "Contacto",
@@ -1212,7 +1213,11 @@
"updated_message": "Tipo de impuesto actualizado correctamente", "updated_message": "Tipo de impuesto actualizado correctamente",
"deleted_message": "Tipo de impuesto eliminado correctamente", "deleted_message": "Tipo de impuesto eliminado correctamente",
"confirm_delete": "No podrá recuperar este tipo de impuesto", "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": { "payment_modes": {
"title": "Formas de pago", "title": "Formas de pago",
@@ -1608,6 +1613,7 @@
"pdf_discount_label": "Descuento", "pdf_discount_label": "Descuento",
"pdf_amount_label": "Cantidad", "pdf_amount_label": "Cantidad",
"pdf_subtotal": "Subtotal", "pdf_subtotal": "Subtotal",
"pdf_net_total": "Base Imponible",
"pdf_total": "Total", "pdf_total": "Total",
"pdf_payment_label": "Pago", "pdf_payment_label": "Pago",
"pdf_payment_receipt_label": "RECIBO DE PAGO", "pdf_payment_receipt_label": "RECIBO DE PAGO",

View File

@@ -217,6 +217,7 @@
"total": "Total", "total": "Total",
"discount": "Desconto", "discount": "Desconto",
"sub_total": "Subtotal", "sub_total": "Subtotal",
"net_total": "Total Líquido",
"estimate_number": "Numero do Orçamento", "estimate_number": "Numero do Orçamento",
"ref_number": "Referência", "ref_number": "Referência",
"contact": "Contato", "contact": "Contato",
@@ -759,7 +760,11 @@
"updated_message": "Tipo de Imposto Atualizado com sucesso", "updated_message": "Tipo de Imposto Atualizado com sucesso",
"deleted_message": "Tipo de Imposto Deletado com sucesso", "deleted_message": "Tipo de Imposto Deletado com sucesso",
"confirm_delete": "Você não poderá recuperar este tipo de Imposto", "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": { "expense_category": {
"title": "Categoria de Despesa", "title": "Categoria de Despesa",
@@ -932,5 +937,6 @@
"address_maxlength": "O endereço não deve ter mais que 255 caracteres.", "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.", "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." "prefix_maxlength": "O prefixo não deve ter mais que 5 caracteres."
} },
"pdf_net_total": "Total Líquido"
} }

View File

@@ -1212,7 +1212,9 @@
"updated_message": "Tipo de Imposto Atualizado com sucesso", "updated_message": "Tipo de Imposto Atualizado com sucesso",
"deleted_message": "Tipo de Imposto Deletado com sucesso", "deleted_message": "Tipo de Imposto Deletado com sucesso",
"confirm_delete": "Você não poderá recuperar este tipo de Imposto", "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": { "payment_modes": {
"title": "Modos de Pagamento", "title": "Modos de Pagamento",

View File

@@ -169,6 +169,9 @@ const taxAmount = computed(() => {
if (taxPerItemEnabled && !discountPerItemEnabled){ if (taxPerItemEnabled && !discountPerItemEnabled){
return getTaxAmount() 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 (props.discountedTotal * localTax.percent) / 100
} }
return 0 return 0
@@ -261,6 +264,7 @@ function getTaxAmount() {
const itemTotal = props.discountedTotal const itemTotal = props.discountedTotal
const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0 const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0
const type = props.store[props.storeProp].discount_type const type = props.store[props.storeProp].discount_type
let discountedTotal = props.discountedTotal
if (modelDiscount > 0) { if (modelDiscount > 0) {
props.store[props.storeProp].items.forEach((_i) => { props.store[props.storeProp].items.forEach((_i) => {
total += _i.total total += _i.total
@@ -268,10 +272,14 @@ function getTaxAmount() {
const proportion = (itemTotal / total).toFixed(2) const proportion = (itemTotal / total).toFixed(2)
discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100 discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100
const itemDiscount = Math.round(discount * proportion) const itemDiscount = Math.round(discount * proportion)
const discounted = itemTotal - itemDiscount discountedTotal = itemTotal - itemDiscount
return Math.round((discounted * localTax.percent) / 100)
} }
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> </script>

View File

@@ -1,4 +1,27 @@
<template> <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"> <table class="text-center item-table min-w-full">
<colgroup> <colgroup>
<col style="width: 40%; min-width: 280px" /> <col style="width: 40%; min-width: 280px" />
@@ -191,4 +214,14 @@ const defaultCurrency = computed(() => {
return companyStore.selectedCompanyCurrency 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> </script>

View File

@@ -38,6 +38,17 @@
</label> </label>
</div> </div>
<div
v-if="store[storeProp].tax_per_item === 'YES'"
>
<NetTotal
:currency="currency"
:store="store"
:storeProp="storeProp"
:isLoading="isLoading"
/>
</div>
<div <div
v-for="tax in itemWiseTaxes" v-for="tax in itemWiseTaxes"
:key="tax.tax_type_id" :key="tax.tax_type_id"
@@ -135,6 +146,21 @@
</div> </div>
</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 <div
v-if=" v-if="
store[storeProp].tax_per_item === 'NO' || store[storeProp].tax_per_item === 'NO' ||
@@ -149,6 +175,7 @@
:taxes="taxes" :taxes="taxes"
:currency="currency" :currency="currency"
:store="store" :store="store"
:storeProp="storeProp"
@remove="removeTax" @remove="removeTax"
@update="updateTax" @update="updateTax"
/> />
@@ -198,6 +225,7 @@
<script setup> <script setup>
import { computed, inject, ref, watch } from 'vue' import { computed, inject, ref, watch } from 'vue'
import Guid from 'guid' import Guid from 'guid'
import NetTotal from './NetTotal.vue'
import Tax from './CreateTotalTaxes.vue' import Tax from './CreateTotalTaxes.vue'
import TaxStub from '@/scripts/admin/stub/abilities' import TaxStub from '@/scripts/admin/stub/abilities'
import SelectTaxPopup from './SelectTaxPopup.vue' import SelectTaxPopup from './SelectTaxPopup.vue'

View File

@@ -42,6 +42,10 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
storeProp: {
type: String,
default: '',
},
data: { data: {
type: String, type: String,
default: '', default: '',
@@ -64,6 +68,13 @@ const taxAmount = computed(() => {
100 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) { if (props.store.getSubtotalWithDiscount && props.tax.percent) {
return Math.round( return Math.round(
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100 (props.store.getSubtotalWithDiscount * props.tax.percent) / 100
@@ -81,6 +92,13 @@ watchEffect(() => {
} }
}) })
watch(
() => props.store[props.storeProp].tax_included,
(val) => {
updateTax()
}, { deep: true },
)
function updateTax() { function updateTax() {
emit('update', { emit('update', {
...props.tax, ...props.tax,

View File

@@ -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>

View File

@@ -44,6 +44,9 @@ export const useEstimateStore = (useWindow = false) => {
return a + b['total'] return a + b['total']
}, 0) }, 0)
}, },
getNetTotal() {
return this.getSubtotalWithDiscount - this.getTotalTax
},
getTotalSimpleTax() { getTotalSimpleTax() {
return _.sumBy(this.newEstimate.taxes, function (tax) { return _.sumBy(this.newEstimate.taxes, function (tax) {
if (!tax.compound_tax) { if (!tax.compound_tax) {
@@ -79,6 +82,9 @@ export const useEstimateStore = (useWindow = false) => {
}, },
getTotal() { getTotal() {
if (this.newEstimate.tax_included) {
return this.getSubtotalWithDiscount
}
return this.getSubtotalWithDiscount + this.getTotalTax return this.getSubtotalWithDiscount + this.getTotalTax
}, },
@@ -149,7 +155,7 @@ export const useEstimateStore = (useWindow = false) => {
resolve(response) resolve(response)
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
handleError(err) handleError(err)
reject(err) reject(err)
}) })
@@ -160,20 +166,19 @@ export const useEstimateStore = (useWindow = false) => {
Object.assign(this.newEstimate, estimate) Object.assign(this.newEstimate, estimate)
if (this.newEstimate.tax_per_item === 'YES') { if (this.newEstimate.tax_per_item === 'YES') {
this.newEstimate.items.forEach((_i) => { this.newEstimate.items.forEach((_i) => {
if (_i.taxes && !_i.taxes.length){ if (_i.taxes && !_i.taxes.length) {
_i.taxes.push({ ...taxStub, id: Guid.raw() }) _i.taxes.push({ ...taxStub, id: Guid.raw() })
} }
}) })
} }
if (this.newEstimate.discount_per_item === 'YES') { if (this.newEstimate.discount_per_item === 'YES') {
this.newEstimate.items.forEach((_i, index) => { this.newEstimate.items.forEach((_i, index) => {
if (_i.discount_type === 'fixed'){ if (_i.discount_type === 'fixed') {
this.newEstimate.items[index].discount = _i.discount / 100 this.newEstimate.items[index].discount = _i.discount / 100
} }
}) })
} } else {
else { if (this.newEstimate.discount_type === 'fixed') {
if (this.newEstimate.discount_type === 'fixed'){
this.newEstimate.discount = this.newEstimate.discount / 100 this.newEstimate.discount = this.newEstimate.discount / 100
} }
} }
@@ -182,19 +187,23 @@ export const useEstimateStore = (useWindow = false) => {
setCustomerAddresses(customer) { setCustomerAddresses(customer) {
const customer_business = customer.customer_business const customer_business = customer.customer_business
if (customer_business?.billing_address){ if (customer_business?.billing_address) {
this.newEstimate.customer.billing_address = customer_business.billing_address this.newEstimate.customer.billing_address =
customer_business.billing_address
} }
if (customer_business?.shipping_address){ if (customer_business?.shipping_address) {
this.newEstimate.customer.shipping_address = customer_business.shipping_address this.newEstimate.customer.shipping_address =
customer_business.shipping_address
} }
}, },
addSalesTaxUs() { addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub } 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) { if (found) {
for (const key in found) { for (const key in found) {
if (Object.prototype.hasOwnProperty.call(salesTax, key)) { if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
@@ -202,10 +211,10 @@ export const useEstimateStore = (useWindow = false) => {
} }
} }
salesTax.id = found.tax_type_id salesTax.id = found.tax_type_id
console.log(salesTax, 'salesTax'); console.log(salesTax, 'salesTax')
taxTypeStore.taxTypes.push(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) .post(`/api/v1/estimates/delete`, id)
.then((response) => { .then((response) => {
let index = this.estimates.findIndex( let index = this.estimates.findIndex(
(estimate) => estimate.id === id (estimate) => estimate.id === id,
) )
this.estimates.splice(index, 1) this.estimates.splice(index, 1)
@@ -288,7 +297,7 @@ export const useEstimateStore = (useWindow = false) => {
.then((response) => { .then((response) => {
this.selectedEstimates.forEach((estimate) => { this.selectedEstimates.forEach((estimate) => {
let index = this.estimates.findIndex( let index = this.estimates.findIndex(
(_est) => _est.id === estimate.id (_est) => _est.id === estimate.id,
) )
this.estimates.splice(index, 1) this.estimates.splice(index, 1)
}) })
@@ -313,7 +322,7 @@ export const useEstimateStore = (useWindow = false) => {
.put(`/api/v1/estimates/${data.id}`, data) .put(`/api/v1/estimates/${data.id}`, data)
.then((response) => { .then((response) => {
let pos = this.estimates.findIndex( 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 this.estimates[pos] = response.data.data
const notificationStore = useNotificationStore() const notificationStore = useNotificationStore()
@@ -355,7 +364,7 @@ export const useEstimateStore = (useWindow = false) => {
.post(`/api/v1/estimates/${data.id}/status`, data) .post(`/api/v1/estimates/${data.id}/status`, data)
.then((response) => { .then((response) => {
let pos = this.estimates.findIndex( let pos = this.estimates.findIndex(
(estimate) => estimate.id === data.id (estimate) => estimate.id === data.id,
) )
if (this.estimates[pos]) { if (this.estimates[pos]) {
this.estimates[pos].status = 'ACCEPTED' this.estimates[pos].status = 'ACCEPTED'
@@ -402,7 +411,7 @@ export const useEstimateStore = (useWindow = false) => {
.post(`/api/v1/estimates/${data.id}/status`, data) .post(`/api/v1/estimates/${data.id}/status`, data)
.then((response) => { .then((response) => {
let pos = this.estimates.findIndex( let pos = this.estimates.findIndex(
(estimate) => estimate.id === data.id (estimate) => estimate.id === data.id,
) )
if (this.estimates[pos]) { if (this.estimates[pos]) {
this.estimates[pos].status = 'SENT' this.estimates[pos].status = 'SENT'
@@ -570,17 +579,26 @@ export const useEstimateStore = (useWindow = false) => {
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes() await notesStore.fetchNotes()
this.newEstimate.notes = notesStore.getDefaultNoteForType('Estimate')?.notes this.newEstimate.notes =
notesStore.getDefaultNoteForType('Estimate')?.notes
this.newEstimate.tax_per_item = this.newEstimate.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newEstimate.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type this.newEstimate.sales_tax_type =
this.newEstimate.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type companyStore.selectedCompanySettings.sales_tax_type
this.newEstimate.sales_tax_address_type =
companyStore.selectedCompanySettings.sales_tax_address_type
this.newEstimate.discount_per_item = this.newEstimate.discount_per_item =
companyStore.selectedCompanySettings.discount_per_item companyStore.selectedCompanySettings.discount_per_item
this.newEstimate.estimate_date = moment().format('YYYY-MM-DD') 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() 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') .format('YYYY-MM-DD')
} }
} else { } else {
@@ -607,9 +625,10 @@ export const useEstimateStore = (useWindow = false) => {
} }
this.setTemplate(this.templates[0].name) this.setTemplate(this.templates[0].name)
this.newEstimate.template_name = this.newEstimate.template_name = userStore.currentUserSettings
userStore.currentUserSettings.default_estimate_template ? .default_estimate_template
userStore.currentUserSettings.default_estimate_template : this.newEstimate.template_name ? userStore.currentUserSettings.default_estimate_template
: this.newEstimate.template_name
} }
if (isEdit) { if (isEdit) {

View File

@@ -51,6 +51,10 @@ export const useInvoiceStore = (useWindow = false) => {
}, 0) }, 0)
}, },
getNetTotal() {
return this.getSubtotalWithDiscount - this.getTotalTax
},
getTotalSimpleTax() { getTotalSimpleTax() {
return _.sumBy(this.newInvoice.taxes, function (tax) { return _.sumBy(this.newInvoice.taxes, function (tax) {
if (!tax.compound_tax) { if (!tax.compound_tax) {
@@ -86,6 +90,9 @@ export const useInvoiceStore = (useWindow = false) => {
}, },
getTotal() { getTotal() {
if (this.newInvoice.tax_included) {
return this.getSubtotalWithDiscount
}
return this.getSubtotalWithDiscount + this.getTotalTax return this.getSubtotalWithDiscount + this.getTotalTax
}, },
@@ -160,8 +167,7 @@ export const useInvoiceStore = (useWindow = false) => {
if (_i.discount_type === 'fixed') if (_i.discount_type === 'fixed')
this.newInvoice.items[index].discount = _i.discount / 100 this.newInvoice.items[index].discount = _i.discount / 100
}) })
} } else {
else {
if (this.newInvoice.discount_type === 'fixed') if (this.newInvoice.discount_type === 'fixed')
this.newInvoice.discount = this.newInvoice.discount / 100 this.newInvoice.discount = this.newInvoice.discount / 100
} }
@@ -171,16 +177,20 @@ export const useInvoiceStore = (useWindow = false) => {
const customer_business = customer.customer_business const customer_business = customer.customer_business
if (customer_business?.billing_address) 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) if (customer_business?.shipping_address)
this.newInvoice.customer.shipping_address = customer_business.shipping_address this.newInvoice.customer.shipping_address =
customer_business.shipping_address
}, },
addSalesTaxUs() { addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
let salesTax = { ...taxStub } 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) { if (found) {
for (const key in found) { for (const key in found) {
if (Object.prototype.hasOwnProperty.call(salesTax, key)) { if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
@@ -237,7 +247,7 @@ export const useInvoiceStore = (useWindow = false) => {
.post(`/api/v1/invoices/delete`, id) .post(`/api/v1/invoices/delete`, id)
.then((response) => { .then((response) => {
let index = this.invoices.findIndex( let index = this.invoices.findIndex(
(invoice) => invoice.id === id (invoice) => invoice.id === id,
) )
this.invoices.splice(index, 1) this.invoices.splice(index, 1)
@@ -261,7 +271,7 @@ export const useInvoiceStore = (useWindow = false) => {
.then((response) => { .then((response) => {
this.selectedInvoices.forEach((invoice) => { this.selectedInvoices.forEach((invoice) => {
let index = this.invoices.findIndex( let index = this.invoices.findIndex(
(_inv) => _inv.id === invoice.id (_inv) => _inv.id === invoice.id,
) )
this.invoices.splice(index, 1) this.invoices.splice(index, 1)
}) })
@@ -286,7 +296,7 @@ export const useInvoiceStore = (useWindow = false) => {
.put(`/api/v1/invoices/${data.id}`, data) .put(`/api/v1/invoices/${data.id}`, data)
.then((response) => { .then((response) => {
let pos = this.invoices.findIndex( 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 this.invoices[pos] = response.data.data
@@ -328,7 +338,7 @@ export const useInvoiceStore = (useWindow = false) => {
.post(`/api/v1/invoices/${data.id}/status`, data) .post(`/api/v1/invoices/${data.id}/status`, data)
.then((response) => { .then((response) => {
let pos = this.invoices.findIndex( let pos = this.invoices.findIndex(
(invoices) => invoices.id === data.id (invoices) => invoices.id === data.id,
) )
if (this.invoices[pos]) { if (this.invoices[pos]) {
@@ -493,26 +503,35 @@ export const useInvoiceStore = (useWindow = false) => {
} }
let editActions = [] let editActions = []
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes() await notesStore.fetchNotes()
this.newInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes this.newInvoice.notes =
notesStore.getDefaultNoteForType('Invoice')?.notes
this.newInvoice.tax_per_item = this.newInvoice.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type this.newInvoice.sales_tax_type =
this.newInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_type companyStore.selectedCompanySettings.sales_tax_type
this.newInvoice.sales_tax_address_type =
companyStore.selectedCompanySettings.sales_tax_address_type
this.newInvoice.discount_per_item = this.newInvoice.discount_per_item =
companyStore.selectedCompanySettings.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') { if (companyStore.selectedCompanySettings.invoice_use_time === 'YES') {
dateFormat += ' HH:mm' dateFormat += ' HH:mm'
} }
this.newInvoice.invoice_date = moment().format(dateFormat) 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() 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') .format('YYYY-MM-DD')
} }
} else { } else {
@@ -539,9 +558,10 @@ export const useInvoiceStore = (useWindow = false) => {
if (res3.data) { if (res3.data) {
this.setTemplate(this.templates[0].name) this.setTemplate(this.templates[0].name)
this.newInvoice.template_name = this.newInvoice.template_name = userStore.currentUserSettings
userStore.currentUserSettings.default_invoice_template ? .default_invoice_template
userStore.currentUserSettings.default_invoice_template : this.newInvoice.template_name ? userStore.currentUserSettings.default_invoice_template
: this.newInvoice.template_name
} }
} }
if (isEdit) { if (isEdit) {

View File

@@ -37,17 +37,56 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
}, },
frequencies: [ 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_minute'),
{ label: global.t('recurring_invoices.frequency.every_hour'), value: '0 * * * *' }, value: '* * * * *',
{ 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_30_minute'),
{ label: global.t('recurring_invoices.frequency.every_15_days_at_midnight'), value: '0 5 */15 * *' }, value: '*/30 * * * *',
{ 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.every_hour'),
{ label: global.t('recurring_invoices.frequency.custom'), value: 'CUSTOM' }, 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() { getTotalSimpleTax() {
return _.sumBy(this.newRecurringInvoice.taxes, function (tax) { return _.sumBy(this.newRecurringInvoice.taxes, function (tax) {
if (!tax.compound_tax) { if (!tax.compound_tax) {
@@ -95,6 +138,9 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
}, },
getTotal() { getTotal() {
if (this.newRecurringInvoice.tax_included) {
return this.getSubtotalWithDiscount
}
return this.getSubtotalWithDiscount + this.getTotalTax return this.getSubtotalWithDiscount + this.getTotalTax
}, },
}, },
@@ -174,7 +220,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
}) })
let pos = this.recurringInvoices.findIndex( 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 this.recurringInvoices[pos] = response.data.data
@@ -240,7 +286,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
.post(`/api/v1/recurring-invoices/delete`, id) .post(`/api/v1/recurring-invoices/delete`, id)
.then((response) => { .then((response) => {
let index = this.recurringInvoices.findIndex( let index = this.recurringInvoices.findIndex(
(invoice) => invoice.id === id (invoice) => invoice.id === id,
) )
this.recurringInvoices.splice(index, 1) this.recurringInvoices.splice(index, 1)
resolve(response) resolve(response)
@@ -265,7 +311,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
.then((response) => { .then((response) => {
this.selectedRecurringInvoices.forEach((invoice) => { this.selectedRecurringInvoices.forEach((invoice) => {
let index = this.recurringInvoices.findIndex( let index = this.recurringInvoices.findIndex(
(_inv) => _inv.id === invoice.id (_inv) => _inv.id === invoice.id,
) )
this.recurringInvoices.splice(index, 1) this.recurringInvoices.splice(index, 1)
}) })
@@ -305,7 +351,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
this.selectAllField = false this.selectAllField = false
} else { } else {
let allInvoiceIds = this.recurringInvoices.map( let allInvoiceIds = this.recurringInvoices.map(
(invoice) => invoice.id (invoice) => invoice.id,
) )
this.selectedRecurringInvoices = allInvoiceIds this.selectedRecurringInvoices = allInvoiceIds
this.selectAllField = true this.selectAllField = true
@@ -351,13 +397,16 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
// on create // on create
if (!isEdit) { if (!isEdit) {
await notesStore.fetchNotes() await notesStore.fetchNotes()
this.newRecurringInvoice.notes = notesStore.getDefaultNoteForType('Invoice')?.notes this.newRecurringInvoice.notes =
notesStore.getDefaultNoteForType('Invoice')?.notes
this.newRecurringInvoice.tax_per_item = this.newRecurringInvoice.tax_per_item =
companyStore.selectedCompanySettings.tax_per_item companyStore.selectedCompanySettings.tax_per_item
this.newRecurringInvoice.discount_per_item = this.newRecurringInvoice.discount_per_item =
companyStore.selectedCompanySettings.discount_per_item companyStore.selectedCompanySettings.discount_per_item
this.newRecurringInvoice.sales_tax_type = companyStore.selectedCompanySettings.sales_tax_type this.newRecurringInvoice.sales_tax_type =
this.newRecurringInvoice.sales_tax_address_type = companyStore.selectedCompanySettings.sales_tax_address_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.starts_at = moment().format('YYYY-MM-DD')
this.newRecurringInvoice.next_invoice_date = moment() this.newRecurringInvoice.next_invoice_date = moment()
.add(7, 'days') .add(7, 'days')
@@ -399,7 +448,7 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
this.isFetchingInitialSettings = false this.isFetchingInitialSettings = false
}) })
.catch((err) => { .catch((err) => {
console.log(err); console.log(err)
handleError(err) handleError(err)
}) })
}, },
@@ -407,7 +456,9 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
addSalesTaxUs() { addSalesTaxUs() {
const taxTypeStore = useTaxTypeStore() const taxTypeStore = useTaxTypeStore()
let salesTax = { ...TaxStub } 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) { if (found) {
for (const key in found) { for (const key in found) {
if (Object.prototype.hasOwnProperty.call(salesTax, key)) { if (Object.prototype.hasOwnProperty.call(salesTax, key)) {
@@ -424,14 +475,15 @@ export const useRecurringInvoiceStore = (useWindow = false) => {
}, },
setSelectedFrequency() { setSelectedFrequency() {
let data = this.frequencies.find( let data = this.frequencies.find((frequency) => {
(frequency) => { return frequency.value === this.newRecurringInvoice.frequency
return frequency.value === this.newRecurringInvoice.frequency })
} data
) ? (this.newRecurringInvoice.selectedFrequency = data)
data ? this.newRecurringInvoice.selectedFrequency = data : (this.newRecurringInvoice.selectedFrequency = {
: this.newRecurringInvoice.selectedFrequency = { label: 'Custom', value: 'CUSTOM' } label: 'Custom',
value: 'CUSTOM',
})
}, },
resetSelectedNote() { resetSelectedNote() {

View File

@@ -8,6 +8,7 @@ export default function () {
customer: null, customer: null,
template_name: '', template_name: '',
tax_per_item: null, tax_per_item: null,
tax_included: false,
sales_tax_type: null, sales_tax_type: null,
sales_tax_address_type: null, sales_tax_address_type: null,
discount_per_item: null, discount_per_item: null,

View File

@@ -20,6 +20,7 @@ export default function () {
sub_total: 0, sub_total: 0,
total: 0, total: 0,
tax_per_item: null, tax_per_item: null,
tax_included: false,
sales_tax_type: null, sales_tax_type: null,
sales_tax_address_type: null, sales_tax_address_type: null,
discount_per_item: null, discount_per_item: null,

View File

@@ -249,6 +249,14 @@ customFieldStore.resetCustomFields()
v$.value.$reset v$.value.$reset
estimateStore.fetchEstimateInitialSettings(isEdit.value) estimateStore.fetchEstimateInitialSettings(isEdit.value)
watch(
() => companyStore.selectedCompanySettings?.tax_included_by_default,
(newVal) => {
estimateStore.newEstimate.tax_included = newVal === 'YES'
},
{immediate: true}
)
async function submitForm() { async function submitForm() {
v$.value.$touch() v$.value.$touch()

View File

@@ -253,6 +253,14 @@ watch(
} }
) )
watch(
() => companyStore.selectedCompanySettings?.tax_included_by_default,
(newVal) => {
invoiceStore.newInvoice.tax_included = newVal === 'YES'
},
{immediate: true}
)
async function submitForm() { async function submitForm() {
v$.value.$touch() v$.value.$touch()

View File

@@ -277,6 +277,14 @@ watch(
} }
) )
watch(
() => companyStore.selectedCompanySettings?.tax_included_by_default,
(newVal) => {
recurringInvoiceStore.newRecurringInvoice.tax_included = newVal === 'YES'
},
{immediate: true}
)
async function submitForm() { async function submitForm() {
v$.value.$touch() v$.value.$touch()

View File

@@ -52,6 +52,21 @@
:title="$t('settings.tax_types.tax_per_item')" :title="$t('settings.tax_types.tax_per_item')"
:description="$t('settings.tax_types.tax_setting_description')" :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> </div>
</BaseSettingCard> </BaseSettingCard>
</template> </template>
@@ -139,6 +154,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() { function hasAtleastOneAbility() {
return userStore.hasAbilities([ return userStore.hasAbilities([
abilities.DELETE_TAX_TYPE, abilities.DELETE_TAX_TYPE,

View File

@@ -101,7 +101,18 @@
</tr> </tr>
@endif @endif
@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') @if ($estimate->tax_per_item === 'YES')
@foreach ($taxes as $tax) @foreach ($taxes as $tax)
<tr> <tr>
@@ -133,7 +144,7 @@
</tr> </tr>
@endforeach @endforeach
@endif @endif
<tr> <tr>
<td class="py-3"></td> <td class="py-3"></td>
<td class="py-3"></td> <td class="py-3"></td>

View File

@@ -121,6 +121,17 @@
@endif @endif
@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') @if ($invoice->tax_per_item === 'YES')
@foreach ($taxes as $tax) @foreach ($taxes as $tax)
<tr> <tr>

View File

@@ -105,6 +105,8 @@ test('update settings', function () {
'notify_invoice_viewed' => 'YES', 'notify_invoice_viewed' => 'YES',
'notify_estimate_viewed' => 'YES', 'notify_estimate_viewed' => 'YES',
'tax_per_item' => 'YES', 'tax_per_item' => 'YES',
'tax_included' => 'YES',
'tax_included_by_default' => 'YES',
'discount_per_item' => 'YES', 'discount_per_item' => 'YES',
]; ];

View File

@@ -463,3 +463,23 @@ test('update estimate with EUR currency', function () {
$response->assertStatus(200); $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'],
]);
});

View File

@@ -520,3 +520,20 @@ test('update invoice with EUR currency', function () {
'base_amount' => $invoice2['taxes'][0]['base_amount'], '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,
]);
});