<template>
<tr>
<td
v-if=“!$vuetify.breakpoint.mobile”
width=“40%”
class=“d-block d-sm-table-cell”
>
<div>
<v-autocomplete
outlined
:items=“items”
:disabled=“disabled”
item-value=“id”
item-text=“name”
cache-items
eager
dense
v-model=“currentItem”
@change=“applyItem”
return-object
:no-data-text=“$t(‘main.empty_product_service’)”
>
</v-autocomplete>
<label>Description</label>
<v-textarea
class=“text-md”
:disabled=“disabled”
v-model=“description”
@change=“itemDescriptionChange”
filled
outlined
auto-grow
rows=“2”
dense
></v-textarea>
</div>
</td>
<td
v-if=“!$vuetify.breakpoint.mobile”
class=“d-block d-sm-table-cell”
width=“25%”
>
<div>
<v-text-field
type=“number”
dense
outlined
:disabled=“disabled”
@keyup=“computeAmount”
v-model=“rawUnit_price”
:prefix=“invoiceSymbol”
@change=“itemPriceChange”
>
</v-text-field>
<label>Tax</label>
<v-select
:disabled=“disabled”
:items=“taxes”
dense
item-value=“id”
multiple
item-text=“display_name”
:hint=“tax_amount | toMoney | invoiceCurrencySymbol”
v-model=“applied_taxes”
@change=“calculateTax”
persistent-hint
>
<template v-slot:prepend-item>
<v-btn
style=“text-decoration: none”
to=“/settings?open=taxes”
block
rounded
text
color=“blue”
>Manage Taxes <v-icon small>mdi-arrow-right</v-icon></v-btn
>
</template>
<template v-slot:selection=“{ item, index }“>
<div v-if=“index === 0” class=“chip”>
<span class=“text-sm”>{{ item.name }}</span>
</div>
<span v-if=“index === 1" class=“grey--text text-caption”>
(+{{ applied_taxes.length - 1 }} others)
</span>
</template>
<template v-slot:item=“{ item }“>
<v-checkbox
color=“blue”
:value=“applied_taxes.includes(item.id)”
></v-checkbox>
<span>
{{ item.display_name }}({{ item.rate }}%)
<small
class=“text--disabled d-block”
v-if=“item.type === ‘Compound’”
>
Includes:
<span v-for=“st in item.sub_tax”
>{{ st.name }}({{ st.rate }}%),
</span>
</small>
</span>
</template>
</v-select>
</div>
</td>
<td
v-if=“!$vuetify.breakpoint.mobile”
class=“d-block d-sm-table-cell”
width=“10%”
>
<v-text-field
type=“number”
dense
outlined
:disabled=“disabled”
@keyup=“computeAmount”
@change=“itemQuantityChange”
v-model=“invoice_quantity”
:rules=“requiredRules”
>
</v-text-field>
<v-menu
max-width=“500px”
min-width=“400px”
:close-on-content-click=“false”
transition=“slide-y-transition”
>
<template v-slot:activator=“{ on, attr }“>
<v-btn
v-on=“on”
v-bind=“attr”
color=“blue”
text
:disabled=“!Boolean(currentItem)”
>
Discount:{{ discountAmount | toMoney | invoiceCurrencySymbol }}
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item>
<v-list-item-content>
<v-list-item-title
>Amount:{{
discountAmount | toMoney | invoiceCurrencySymbol
}}</v-list-item-title
>
<v-list-item-subtitle
>{{
Number(discount_percent).toFixed(2)
}}%</v-list-item-subtitle
>
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-switch
color=“blue”
inset
label=“Enter discount % ”
v-model=“percent”
>
</v-switch>
</v-list-item>
<v-list-item v-if=“percent”>
<v-text-field
outlined
label=“Discount percent”
v-model=“discount_percent”
@input=“cumputeDiscountAmount()”
@blur=“computeAmount()”
ref=“discountPinput”
filled
>
</v-text-field>
</v-list-item>
<v-list-item v-else>
<v-text-field
outlined
label=“Absolute discount amount”
v-model=“discount_amount”
@input=“cumputePercentage()”
@blur=“computeAmount()”
ref=“discount_amount_input”
filled
>
</v-text-field>
</v-list-item>
</v-list>
</v-menu>
</td>
<td
v-if=“!$vuetify.breakpoint.mobile”
class=“d-block d-sm-table-cell”
width=“40%”
>
<div class=“d-flex justify-start”>
<p class=“m-0”>
{{ invoice_amount.toFixed(2) | toMoney | invoiceCurrencySymbol }}
</p>
<v-btn color=“red” icon small class=“ml-3 m-0 up” @click=“removeItem”>
<v-icon>mdi-trash-can</v-icon>
</v-btn>
</div>
<br />
<br />
<strong
>Amount due:{{
(Number(amount_due) + Number(tax_amount))
| toMoney
| invoiceCurrencySymbol
}}</strong
>
</td>
<!-- only way to make responsive is to have separate td for mobile -->
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell mb-5">
<v-autocomplete
outlined
label=“Item”
:items=“items”
:disabled=“disabled”
item-value=“id”
item-text=“name”
cache-items
eager
dense
v-model=“currentItem”
@change=“applyItem”
return-object
:no-data-text=“$t(‘main.empty_product_service’)”
>
</v-autocomplete>
</td>
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell mb-5">
<v-textarea
class=“text-md”
:disabled=“disabled”
v-model=“description”
label=“Description”
@change=“itemDescriptionChange”
filled
outlined
auto-grow
rows=“2"
dense
></v-textarea>
</td>
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell mb-5">
<v-text-field
type=“number”
dense
label=“Price”
outlined
:disabled=“disabled”
@keyup=“computeAmount”
v-model=“rawUnit_price”
:prefix=“invoiceSymbol”
@change=“itemPriceChange”
>
</v-text-field>
</td>
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell mb-5”>
<v-select
:disabled=“disabled”
:items=“taxes”
dense
item-value=“id”
multiple
label=“Tax Amount”
item-text=“display_name”
:hint=“tax_amount | toMoney | invoiceCurrencySymbol”
v-model=“applied_taxes”
@change=“calculateTax”
persistent-hint
>
<template v-slot:selection=“{ item, index }“>
<div v-if=“index === 0” small class=“chip”>
<span class=“text-sm”>{{ item.name }}</span>
</div>
<span v-if=“index === 1" class=“grey--text caption”> (+1) </span>
</template>
</v-select>
</td>
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell mb-5”>
<v-text-field
type=“number”
label=“Quantity”
dense
outlined
:disabled=“disabled”
@keyup=“computeAmount”
@change=“itemQuantityChange”
v-model=“invoice_quantity”
:rules=“requiredRules”
>
</v-text-field>
</td>
<td v-if=“$vuetify.breakpoint.mobile” class=“d-block d-sm-table-cell”>
<p class=“m-0”>
Amount: {{ invoice_amount.toFixed(4) | invoiceCurrencySymbol }}
</p>
</td>
<td
v-if=“$vuetify.breakpoint.mobile”
class=“d-block d-sm-table-cell d-flex justify-center”
>
<v-btn color=“red” icon large @click=“removeItem”>
<v-icon>mdi-trash-can</v-icon>
</v-btn>
</td>
</tr>
</template>
<script>
export default {
name: “invoiceItem”,
props: [“items”, “id”, “defaultItem”, “disabled”, “index”],
data() {
return {
discount_percent_input: 0,
discount_percent: 0,
discount_amount_input: 0,
discount_amount: 0,
percent: true,
currentItem: null,
description: “”,
rawUnit_price: “”,
applied_taxes: [],
invoice_quantity: 0,
tax_amount: 0,
invoice_amount: 0,
quantity_error: false,
requiredRules: [(v) => !!v || this.$t(“main.required”)],
};
},
watch: {
discount_percent_input() {
this.cumputeDiscountAmount();
this.cumputePercentage();
},
discount_amount_input() {
this.cumputeDiscountAmount();
this.cumputePercentage();
},
defaultItem() {
if (this.defaultItem) {
this.currentItem = this.defaultItem;
//this.applyItem();
//this.computeAmount();
}
},
invoice_amount() {
this.computeAmount();
},
},
computed: {
amount_due() {
return (
Number(this.invoice_amount) - Number(this.discount_amount)
).toFixed(2);
},
taxes() {
return this.$store.state.user.user_infor.current_business.taxes;
},
invoiceSymbol() {
return this.$store.state.invoiceCurrencySymbol;
},
},
methods: {
cumputePercentage() {
const amount = Number(this.invoice_amount);
const d = Number(this.discount_amount);
const p = d > 0 ? (d / amount) * 100 : 0;
this.discount_percent = p;
return p;
},
cumputeDiscountAmount() {
const amount = Number(this.invoice_amount);
const p = Number(this.discount_percent);
this.discount_amount =
Number(p) > 0 ? ((Number(p) / 100) * Number(amount)).toFixed(2) : 0;
return this.discount_amount;
},
getUpdatedItem() {
return {
...this.currentItem,
description: this.description,
unit_price: this.rawUnit_price,
taxes: this.taxes,
applied_taxes: this.applied_taxes,
invoice_quantity: this.invoice_quantity,
tax_amount: this.tax_amount,
invoice_amount: this.invoice_amount,
quantity_error: this.quantity_error,
amount_due: Number(this.amount_due) + Number(this.tax_amount),
discount_amount: this.discount_amount,
discount_percent: this.discount_percent,
};
},
applyItem() {
if (this.currentItem && this.currentItem.id) {
this.description = this.currentItem.description;
this.rawUnit_price = this.currentItem.rawUnit_price;
this.applied_taxes = this.currentItem.applied_taxes;
this.invoice_quantity = this.currentItem.invoice_quantity;
this.invoice_amount = this.currentItem.invoice_amount;
this.discount_amount = this.currentItem.discount_amount;
this.discount_percent = this.currentItem.discount_percent;
this.calculateTax();
}
},
removeItem() {
this.$emit(“remove”, this.index);
},
itemDescriptionChange() {
this.$emit(“change”, this.id, this.getUpdatedItem());
},
computeAmount() {
this.cumputeDiscountAmount();
this.cumputePercentage();
if (this.invoice_quantity) {
this.invoice_amount =
Number(this.rawUnit_price) * Number(this.invoice_quantity);
}
this.calculateTax();
this.$emit(“change”, this.id, this.getUpdatedItem());
},
generateErrorMsg() {
const item = this.currentItem;
return `Invalid quantity provided for ${item.name}, you have ${item.quantity} of ${item.name} in stock`;
},
hasQuantityError() {
if (
this.currentItem &&
this.currentItem.track_inventory === 1 &&
this.invoice_quantity > this.currentItem.quantity
) {
this.errorMsg = this.generateErrorMsg();
this.quantity_error = true;
return true;
}
this.quantity_error = false;
return false;
},
calculateTaxes() {
const taxObjs = this.taxes.filter((tax) =>
this.applied_taxes.includes(tax.id)
);
let taxAmount = 0;
taxObjs.forEach((taxObj) => {
if (taxObj.type === “Flat”) {
taxAmount += (Number(taxObj.rate) / 100) * Number(this.amount_due);
} else if (taxObj.type === “Compound”) {
const initialTax =
(Number(taxObj.sub_rate) / 100) * Number(this.amount_due);
const compoundAmount = initialTax + Number(this.amount_due);
const compoundTax =
(Number(taxObj.rate) / 100) * Number(compoundAmount);
taxAmount += compoundTax + initialTax;
}
});
return taxAmount;
},
calculateTax() {
this.tax_amount = this.calculateTaxes();
this.$emit(“change”, this.id, this.getUpdatedItem());
},
itemPriceChange() {
this.rawUnit_price
? (this.rawUnit_price = this.rawUnit_price)
: (this.rawUnit_price = 0);
this.computeAmount();
},
itemQuantityChange() {
this.invoice_quantity
? (this.invoice_quantity = this.invoice_quantity)
: (this.invoice_quantity = 1);
this.computeAmount();
},
},
mounted() {
if (this.defaultItem) {
this.currentItem = this.defaultItem;
this.applyItem();
}
},
};
</script>
<style scoped>
td,
td * {
vertical-align: top !important;
margin-top: 0.5rem !important;
}
.text-sm {
font-size: 11px;
}
.text-md {
font-size: 14px;
}
.text-bold {
font-weight: 800;
}
.up {
transform: translateY(-0.5rem);
}
.chip {
padding: 0.3rem 1rem;
background-color: #eeeeee;
border-radius: 6rem;
}
</style>
simple/src/components/invoices/invoiceItem.vue