feat : item enquiry

This commit is contained in:
Gadha2311
2025-07-21 09:56:34 +05:30
parent cf6b028218
commit d661ea04a7
12 changed files with 498 additions and 0 deletions
@@ -23,6 +23,7 @@ export class AccountingSettings extends Doc {
enableInvoiceReturns?: boolean;
enableLoyaltyProgram?: boolean;
enablePricingRule?: boolean;
enaenableItemEnquiry?: boolean;
enableERPNextSync?: boolean;
enablePointOfSaleWithOutInventory?: boolean;
enablePartialPayment?: boolean;
@@ -0,0 +1,22 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';
export class ItemEnquiry extends Doc {
itemName!: string;
customerName!: string;
contact?: string;
description?: string;
similarProduct?: string;
static override getListViewSettings(): ListViewSettings {
return {
columns: [
'itemName',
'customerName',
'contact',
'description',
'similarProduct',
],
};
}
}
+2
View File
@@ -57,6 +57,7 @@ import { ERPNextSyncSettings } from './baseModels/ERPNextSyncSettings/ERPNextSyn
import { ERPNextSyncQueue } from './baseModels/ERPNextSyncQueue/ERPNextSyncQueue';
import { FetchFromERPNextQueue } from './baseModels/FetchFromERPNextQueue/FetchFromERPNextQueue';
import { IntegrationErrorLog } from './baseModels/IntegrationErrorLog/IntegrationErrorLog';
import { ItemEnquiry } from './baseModels/ItemEnquiry/ItemEnquiry';
export const models = {
Account,
@@ -67,6 +68,7 @@ export const models = {
Defaults,
Item,
ItemGroup,
ItemEnquiry,
JournalEntry,
JournalEntryAccount,
Misc,
+1
View File
@@ -21,6 +21,7 @@ export enum ModelNameEnum {
Defaults = 'Defaults',
Item = 'Item',
ItemGroup = 'ItemGroup',
ItemEnquiry = 'ItemEnquiry',
UOM = 'UOM',
UOMConversionItem = 'UOMConversionItem',
JournalEntry = 'JournalEntry',
+7
View File
@@ -121,6 +121,13 @@
"default": false,
"section": "Features"
},
{
"fieldname": "enableItemEnquiry",
"label": "Enable Item Enquiry",
"fieldtype": "Check",
"default": true,
"section": "Features"
},
{
"fieldname": "enableLoyaltyProgram",
"label": "Enable Loyalty Program",
+42
View File
@@ -0,0 +1,42 @@
{
"name": "ItemEnquiry",
"label": "Item Enquiry",
"create": false,
"isSingle": false,
"isChild": false,
"naming": "autoincrement",
"fields": [
{
"fieldname": "itemName",
"label": "Item Name",
"fieldtype": "Data",
"required": true,
"section": "Default"
},
{
"fieldname": "customerName",
"label": "Customer Name",
"fieldtype": "Data",
"section": "Default"
},
{
"fieldname": "contact",
"label": "Contact",
"fieldtype": "Data",
"section": "Default"
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text",
"section": "Default"
},
{
"fieldname": "similarProduct",
"label": "Similar Product",
"fieldtype": "Data",
"readOnly": true,
"section": "Default"
}
]
}
+2
View File
@@ -81,6 +81,7 @@ import FetchFromERPNextQueue from './app/FetchFromERPNextQueue.json';
import IntegrationErrorLog from './app/IntegrationErrorLog.json';
import ItemGroup from './app/ItemGroup.json';
import { Schema, SchemaStub } from './types';
import ItemEnquiry from './app/ItemEnquiry.json';
export const coreSchemas: Schema[] = [
PatchRun as Schema,
@@ -132,6 +133,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [
JournalEntryAccount as Schema,
Invoice as Schema,
ItemEnquiry as Schema,
SalesInvoice as Schema,
PurchaseInvoice as Schema,
SalesQuote as Schema,
+1
View File
@@ -19,6 +19,7 @@ export const modalNames = [
'Alert',
'CouponCode',
'PriceList',
'ItemEnquiry',
'ReturnSalesInvoice',
] as const;
+8
View File
@@ -40,6 +40,11 @@
@toggle-modal="emitEvent('toggleModal', 'PriceList')"
/>
<ItemEnquiryModal
:open-modal="openItemEnquiryModal"
@toggle-modal="emitEvent('toggleModal', 'ItemEnquiry')"
/>
<PaymentModal
:open-modal="openPaymentModal"
@toggle-modal="emitEvent('toggleModal', 'Payment')"
@@ -372,6 +377,7 @@ import PaymentModal from './PaymentModal.vue';
import Button from 'src/components/Button.vue';
import { defineComponent, PropType } from 'vue';
import PriceListModal from './PriceListModal.vue';
import ItemEnquiryModal from './ItemEnquiryModal.vue';
import { Item } from 'models/baseModels/Item/Item';
import CouponCodeModal from './CouponCodeModal.vue';
import POSQuickActions from './POSQuickActions.vue';
@@ -404,6 +410,7 @@ export default defineComponent({
PaymentModal,
MultiLabelLink,
PriceListModal,
ItemEnquiryModal,
CouponCodeModal,
POSQuickActions,
OpenPOSShiftModal,
@@ -424,6 +431,7 @@ export default defineComponent({
disablePayButton: Boolean,
openPaymentModal: Boolean,
openPriceListModal: Boolean,
openItemEnquiryModal: Boolean,
openCouponCodeModal: Boolean,
openShiftCloseModal: Boolean,
openSavedInvoiceModal: Boolean,
+363
View File
@@ -0,0 +1,363 @@
<template>
<Modal class="h-auto w-96" :set-close-listener="false">
<p class="text-center font-semibold py-3">{{ t`Item Enquiry` }}</p>
<div class="px-10">
<hr class="dark:border-gray-800" />
<div class="flex flex-col gap-5 pt-8">
<div class="relative">
<label class="text-sm font-medium">{{ t`Item Name` }} *</label>
<input
v-model="form.itemName"
type="text"
required
class="
w-full
border
dark:border-gray-700
rounded
px-3
py-2
mt-1
dark:bg-gray-800 dark:text-white
focus:outline-none
"
autocomplete="off"
@input="suggestSimilarProduct"
@focus="dropdownOpen = true"
@blur="dropdownOpen = false"
/>
<ul
v-if="dropdownOpen"
class="
absolute
z-50
bg-white
dark:bg-gray-800
shadow-md
border border-gray-300
dark:border-gray-700
w-40
max-h-40
overflow-y-auto
rounded
mt-1
"
>
<li
v-for="item in itemOptions"
:key="item.name"
class="
px-3
py-1
text-sm text-gray-800
dark:text-white
cursor-pointer
hover:bg-gray-100
dark:hover:bg-gray-700
"
@mousedown.prevent="
form.itemName = item.name;
form.description = item.description || '';
dropdownOpen = false;
suggestSimilarProduct();
"
>
{{ item.name }}
</li>
</ul>
</div>
<div class="relative">
<label class="text-sm font-medium">{{ t`Customer Name` }}</label>
<input
v-model="form.customerName"
type="text"
class="
w-full
border
dark:border-gray-700
rounded
px-3
py-2
mt-1
dark:bg-gray-800 dark:text-white
focus:outline-none
"
autocomplete="off"
@focus="partyDropdownOpen = true"
/>
<ul
v-if="partyDropdownOpen"
class="
absolute
z-50
bg-white
dark:bg-gray-800
shadow-md
border border-gray-300
dark:border-gray-700
w-full
max-h-40
overflow-y-auto
rounded
mt-1
"
>
<li
v-for="party in parties"
:key="party.name"
class="
px-3
py-1
text-sm text-gray-800
dark:text-white
cursor-pointer
hover:bg-gray-100
dark:hover:bg-gray-700
"
@mousedown.prevent="
form.customerName = party.name;
form.contact = party.phone;
partyDropdownOpen = false;
"
>
{{ party.name }}
</li>
</ul>
</div>
<div>
<label class="text-sm font-medium">{{ t`Contact` }}</label>
<input
v-model="form.contact"
type="text"
class="
w-full
border
dark:border-gray-700
rounded
px-3
py-2
mt-1
dark:bg-gray-800 dark:text-white
focus:outline-none focus:ring-0
"
/>
</div>
<div>
<label class="text-sm font-medium">{{ t`Description` }}</label>
<textarea
v-model="form.description"
rows="2"
class="
w-full
border
dark:border-gray-700
rounded
px-3
py-2
mt-1
dark:bg-gray-800 dark:text-white
focus:outline-none focus:ring-0
"
/>
</div>
<div>
<label class="text-sm font-medium">{{ t`Similar Product` }}</label>
<input
v-model="form.similarProduct"
type="text"
class="
w-full
border
dark:border-gray-700
rounded
px-3
py-2
mt-1
dark:bg-gray-800 dark:text-white
focus:outline-none focus:ring-0
"
readonly
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-10 mb-4">
<div class="col-span-2">
<Button
class="w-full bg-green-500 dark:bg-green-700"
style="padding: 1.35rem"
@click="submitForm"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Submit` }}
</p>
</slot>
</Button>
</div>
</div>
<!-- Cancel Button -->
<div class="grid grid-cols-2 gap-4 mb-6">
<div class="col-span-2">
<Button
class="w-full bg-red-500 dark:bg-red-700"
style="padding: 1.35rem"
@click="closeModal"
>
<slot>
<p class="uppercase text-lg text-white font-semibold">
{{ t`Cancel` }}
</p>
</slot>
</Button>
</div>
</div>
</div>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { t } from 'fyo';
import { showToast } from 'src/utils/interactive';
import Modal from 'src/components/Modal.vue';
import Button from 'src/components/Button.vue';
export default defineComponent({
name: 'ItemEnquiryModal',
components: {
Modal,
Button,
},
emits: ['toggleModal'],
data() {
return {
form: {
itemName: '',
customerName: '',
contact: '',
description: '',
similarProduct: '',
},
itemOptions: [] as { name: string; description?: string }[],
dropdownOpen: false,
parties: [] as { name: string; phone: string }[],
partyDropdownOpen: false,
};
},
async mounted() {
await this.fetchItems();
await this.fetchParties();
},
methods: {
async fetchItems() {
try {
const items = await this.fyo.db.getAll('Item', {
filters: {},
fields: ['name', 'description'],
});
this.itemOptions = (items as Record<string, string>[])
.filter((item) => item.name)
.map((item) => ({
name: item.name,
description: item.description,
}));
} catch (error) {
showToast({ type: 'error', message: t`Failed to load items.` });
}
},
async fetchParties() {
try {
const partyData = await this.fyo.db.getAll('Party', {
filters: { role: ['in', ['Customer', 'Both']] },
fields: ['name', 'phone'],
});
this.parties = (partyData as Record<string, string>[])
.filter((p) => p.name && p.phone)
.map((p) => ({
name: p.name,
phone: p.phone,
}));
} catch (error) {
showToast({ type: 'error', message: t`Failed to load customers.` });
}
},
suggestSimilarProduct() {
const input = this.form.itemName.toLowerCase().trim();
if (!input) {
this.form.similarProduct = '';
return;
}
const match = this.itemOptions.find(
(item) =>
input.includes(item.name.toLowerCase()) &&
item.name.toLowerCase() !== input
);
this.form.similarProduct = match ? match.name : '';
},
clearFormValues() {
this.form = {
itemName: '',
customerName: '',
contact: '',
description: '',
similarProduct: '',
};
this.dropdownOpen = false;
this.partyDropdownOpen = false;
},
async submitForm() {
if (!this.form.itemName.trim()) {
showToast({
type: 'error',
message: t`Item Name is required`,
});
return;
}
try {
const doc = this.fyo.doc.getNewDoc('ItemEnquiry');
doc.itemName = this.form.itemName;
doc.customerName = this.form.customerName;
doc.contact = this.form.contact;
doc.description = this.form.description;
doc.similarProduct = this.form.similarProduct;
await doc.sync();
showToast({
type: 'success',
message: t`Item enquiry submitted`,
});
this.clearFormValues();
this.$emit('toggleModal', 'ItemEnquiry');
} catch (error) {
showToast({
type: 'error',
message: t`${error as string}`,
});
}
},
closeModal() {
this.clearFormValues();
this.$emit('toggleModal', 'ItemEnquiry');
},
},
});
</script>
+2
View File
@@ -32,6 +32,7 @@
:item-discounts="(itemDiscounts as Money)"
:coupons="(coupons as AppliedCouponCodes)"
:open-price-list-modal="openPriceListModal"
:open-item-enquiry-modal="openItemEnquiryModal"
:applied-coupons-count="appliedCouponsCount"
:open-shift-close-modal="openShiftCloseModal"
:open-coupon-code-modal="openCouponCodeModal"
@@ -203,6 +204,7 @@ export default defineComponent({
openPaymentModal: false,
openKeyboardModal: false,
openPriceListModal: false,
openItemEnquiryModal: false,
openCouponCodeModal: false,
openShiftCloseModal: false,
openSavedInvoiceModal: false,
+47
View File
@@ -296,6 +296,53 @@
Price List
</span>
</div>
<div
class="relative group"
:class="{
hidden: !fyo.singles.AccountingSettings?.enableItemEnquiry,
}"
>
<div
class="p-1 rounded-md bg-gray-100"
@click="$emit('toggleModal', 'ItemEnquiry')"
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="23px"
viewBox="0 -960 960 960"
width="24px"
fill="#000"
>
<path
d="M180.31-164q-27.01 0-45.66-18.65Q116-201.3 116-228.31v-503.38q0-27.01 18.65-45.66Q153.3-796 180.31-796h599.38q27.01 0 45.66 18.65Q844-758.7 844-731.69v503.38q0 27.01-18.65 45.66Q806.7-164 779.69-164H180.31Zm0-52h599.38q4.62 0 8.46-3.85 3.85-3.84 3.85-8.46v-503.38q0-4.62-3.85-8.46-3.84-3.85-8.46-3.85H180.31q-4.62 0-8.46 3.85-3.85 3.84-3.85 8.46v503.38q0 4.62 3.85 8.46 3.84 3.85 8.46 3.85ZM221-297h172v-52H221v52Zm361-77.23L737.77-530 701-566.77l-119 119-51-51L494.23-462 582-374.23ZM221-454h172v-52H221v52Zm0-156h172v-52H221v52Zm-53 394v-528 528Z"
/>
</svg>
</div>
<span
class="
absolute
bottom-full
left-1/2
transform
-translate-x-1/2
mb-2
bg-gray-100
dark:bg-gray-800 dark:text-white
text-black text-xs
rounded-md
p-2
w-28
text-center
opacity-0
group-hover:opacity-100
transition-opacity
duration-300
"
>
Item Enquiry
</span>
</div>
</template>
<script lang="ts">