diff --git a/backend/patches/updateSchemas.ts b/backend/patches/updateSchemas.ts
index e883de26c..1ad11bae8 100644
--- a/backend/patches/updateSchemas.ts
+++ b/backend/patches/updateSchemas.ts
@@ -21,6 +21,7 @@ const defaultNumberSeriesMap = {
[ModelNameEnum.JournalEntry]: 'JV-',
[ModelNameEnum.SalesInvoice]: 'SINV-',
[ModelNameEnum.PurchaseInvoice]: 'PINV-',
+ [ModelNameEnum.SalesQuote]: 'SQUOT-',
} as Record;
async function execute(dm: DatabaseManager) {
@@ -209,6 +210,7 @@ async function copyTransactionalTables(
ModelNameEnum.Payment,
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
+ ModelNameEnum.SalesQuote,
];
for (const sn of schemaNames) {
diff --git a/models/baseModels/Defaults/Defaults.ts b/models/baseModels/Defaults/Defaults.ts
index 2515d35e8..d590193c0 100644
--- a/models/baseModels/Defaults/Defaults.ts
+++ b/models/baseModels/Defaults/Defaults.ts
@@ -14,6 +14,7 @@ export class Defaults extends Doc {
purchaseReceiptLocation?: string;
// Number Series
+ salesQuoteNumberSeries?: string;
salesInvoiceNumberSeries?: string;
purchaseInvoiceNumberSeries?: string;
journalEntryNumberSeries?: string;
@@ -29,6 +30,7 @@ export class Defaults extends Doc {
purchaseReceiptTerms?: string;
// Print Templates
+ salesQuotePrintTemplate?: string;
salesInvoicePrintTemplate?: string;
purchaseInvoicePrintTemplate?: string;
journalEntryPrintTemplate?: string;
@@ -46,6 +48,9 @@ export class Defaults extends Doc {
salesPaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
purchasePaymentAccount: () => ({ isGroup: false, accountType: 'Cash' }),
// Number Series
+ salesQuoteNumberSeries: () => ({
+ referenceType: ModelNameEnum.SalesQuote,
+ }),
salesInvoiceNumberSeries: () => ({
referenceType: ModelNameEnum.SalesInvoice,
}),
@@ -68,6 +73,7 @@ export class Defaults extends Doc {
referenceType: ModelNameEnum.PurchaseReceipt,
}),
// Print Templates
+ salesQuotePrintTemplate: () => ({ type: ModelNameEnum.SalesQuote }),
salesInvoicePrintTemplate: () => ({ type: ModelNameEnum.SalesInvoice }),
purchaseInvoicePrintTemplate: () => ({
type: ModelNameEnum.PurchaseInvoice,
@@ -118,4 +124,5 @@ export const numberSeriesDefaultsMap: Record<
[ModelNameEnum.StockMovement]: 'stockMovementNumberSeries',
[ModelNameEnum.Shipment]: 'shipmentNumberSeries',
[ModelNameEnum.PurchaseReceipt]: 'purchaseReceiptNumberSeries',
+ [ModelNameEnum.SalesQuote]: 'salesQuoteNumberSeries',
};
diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts
index 04679097d..d89d83a04 100644
--- a/models/baseModels/Invoice/Invoice.ts
+++ b/models/baseModels/Invoice/Invoice.ts
@@ -71,7 +71,13 @@ export abstract class Invoice extends Transactional {
returnAgainst?: string;
get isSales() {
- return this.schemaName === 'SalesInvoice';
+ return (
+ this.schemaName === 'SalesInvoice' || this.schemaName == 'SalesQuote'
+ );
+ }
+
+ get isQuote() {
+ return this.schemaName == 'SalesQuote';
}
get enableDiscounting() {
@@ -493,7 +499,7 @@ export abstract class Invoice extends Transactional {
}
async _updateIsItemsReturned() {
- if (!this.isReturn || !this.returnAgainst) {
+ if (!this.isReturn || !this.returnAgainst || this.isQuote) {
return;
}
@@ -515,7 +521,7 @@ export abstract class Invoice extends Transactional {
}
async _validateHasLinkedReturnInvoices() {
- if (!this.name || this.isReturn) {
+ if (!this.name || this.isReturn || this.isQuote) {
return;
}
@@ -685,6 +691,7 @@ export abstract class Invoice extends Transactional {
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
backReference: () => !this.backReference,
+ quote: () => !this.quote,
priceList: () => !this.fyo.singles.AccountingSettings?.enablePriceList,
returnAgainst: () =>
(this.isSubmitted || this.isCancelled) && !this.returnAgainst,
diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts
index 9c39993dc..6c9371a19 100644
--- a/models/baseModels/InvoiceItem/InvoiceItem.ts
+++ b/models/baseModels/InvoiceItem/InvoiceItem.ts
@@ -47,7 +47,10 @@ export abstract class InvoiceItem extends Doc {
itemTaxedTotal?: Money;
get isSales() {
- return this.schemaName === 'SalesInvoiceItem';
+ return (
+ this.schemaName === 'SalesInvoiceItem' ||
+ this.schemaName === 'SalesQuoteItem'
+ );
}
get date() {
diff --git a/models/baseModels/PrintTemplate.ts b/models/baseModels/PrintTemplate.ts
index 587506430..145401be0 100644
--- a/models/baseModels/PrintTemplate.ts
+++ b/models/baseModels/PrintTemplate.ts
@@ -55,6 +55,7 @@ export class PrintTemplate extends Doc {
const models = [
ModelNameEnum.SalesInvoice,
+ ModelNameEnum.SalesQuote,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
ModelNameEnum.Payment,
diff --git a/models/baseModels/SalesQuote/SalesQuote.ts b/models/baseModels/SalesQuote/SalesQuote.ts
new file mode 100644
index 000000000..2c23731db
--- /dev/null
+++ b/models/baseModels/SalesQuote/SalesQuote.ts
@@ -0,0 +1,67 @@
+import { Fyo } from 'fyo';
+import { DocValueMap } from 'fyo/core/types';
+import { Action, ListViewSettings } from 'fyo/model/types';
+import { ModelNameEnum } from 'models/types';
+import { getQuoteActions, getTransactionStatusColumn } from '../../helpers';
+import { Invoice } from '../Invoice/Invoice';
+import { SalesQuoteItem } from '../SalesQuoteItem/SalesQuoteItem';
+import { Defaults } from '../Defaults/Defaults';
+
+export class SalesQuote extends Invoice {
+ items?: SalesQuoteItem[];
+
+ // This is an inherited method and it must keep the async from the parent
+ // class
+ // eslint-disable-next-line @typescript-eslint/require-await
+ async getPosting() {
+ return null;
+ }
+
+ async getInvoice(): Promise {
+ if (!this.isSubmitted) {
+ return null;
+ }
+
+ const schemaName = ModelNameEnum.SalesInvoice;
+ const defaults = (this.fyo.singles.Defaults as Defaults) ?? {};
+ const terms = defaults.salesInvoiceTerms ?? '';
+ const numberSeries = defaults.salesInvoiceNumberSeries ?? undefined;
+
+ const data: DocValueMap = {
+ ...this.getValidDict(false, true),
+ date: new Date().toISOString(),
+ terms,
+ numberSeries,
+ quote: this.name,
+ items: [],
+ };
+
+ const invoice = this.fyo.doc.getNewDoc(schemaName, data) as Invoice;
+ for (const row of this.items ?? []) {
+ await invoice.append('items', row.getValidDict(false, true));
+ }
+
+ if (!invoice.items?.length) {
+ return null;
+ }
+
+ return invoice;
+ }
+
+ static getListViewSettings(): ListViewSettings {
+ return {
+ columns: [
+ 'name',
+ getTransactionStatusColumn(),
+ 'party',
+ 'date',
+ 'baseGrandTotal',
+ 'outstandingAmount',
+ ],
+ };
+ }
+
+ static getActions(fyo: Fyo): Action[] {
+ return getQuoteActions(fyo, ModelNameEnum.SalesQuote);
+ }
+}
diff --git a/models/baseModels/SalesQuoteItem/SalesQuoteItem.ts b/models/baseModels/SalesQuoteItem/SalesQuoteItem.ts
new file mode 100644
index 000000000..a2f2133c2
--- /dev/null
+++ b/models/baseModels/SalesQuoteItem/SalesQuoteItem.ts
@@ -0,0 +1,3 @@
+import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
+
+export class SalesQuoteItem extends InvoiceItem {}
diff --git a/models/helpers.ts b/models/helpers.ts
index 19f32bda7..c5a25f49d 100644
--- a/models/helpers.ts
+++ b/models/helpers.ts
@@ -11,10 +11,18 @@ import {
} from './baseModels/Account/types';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { Invoice } from './baseModels/Invoice/Invoice';
+import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
import { StockMovement } from './inventory/StockMovement';
import { StockTransfer } from './inventory/StockTransfer';
import { InvoiceStatus, ModelNameEnum } from './types';
+export function getQuoteActions(
+ fyo: Fyo,
+ schemaName: ModelNameEnum.SalesQuote
+): Action[] {
+ return [getMakeInvoiceAction(fyo, schemaName)];
+}
+
export function getInvoiceActions(
fyo: Fyo,
schemaName: ModelNameEnum.SalesInvoice | ModelNameEnum.PurchaseInvoice
@@ -67,7 +75,10 @@ export function getMakeStockTransferAction(
export function getMakeInvoiceAction(
fyo: Fyo,
- schemaName: ModelNameEnum.Shipment | ModelNameEnum.PurchaseReceipt
+ schemaName:
+ | ModelNameEnum.Shipment
+ | ModelNameEnum.PurchaseReceipt
+ | ModelNameEnum.SalesQuote
): Action {
let label = fyo.t`Sales Invoice`;
if (schemaName === ModelNameEnum.PurchaseReceipt) {
@@ -77,9 +88,15 @@ export function getMakeInvoiceAction(
return {
label,
group: fyo.t`Create`,
- condition: (doc: Doc) => doc.isSubmitted && !doc.backReference,
+ condition: (doc: Doc) => {
+ if (schemaName === ModelNameEnum.SalesQuote) {
+ return doc.isSubmitted;
+ } else {
+ return doc.isSubmitted && !doc.backReference;
+ }
+ },
action: async (doc: Doc) => {
- const invoice = await (doc as StockTransfer).getInvoice();
+ const invoice = await (doc as SalesQuote | StockTransfer).getInvoice();
if (!invoice || !invoice.name) {
return;
}
diff --git a/models/index.ts b/models/index.ts
index 51c1c6723..77edb6260 100644
--- a/models/index.ts
+++ b/models/index.ts
@@ -19,6 +19,8 @@ import { PurchaseInvoice } from './baseModels/PurchaseInvoice/PurchaseInvoice';
import { PurchaseInvoiceItem } from './baseModels/PurchaseInvoiceItem/PurchaseInvoiceItem';
import { SalesInvoice } from './baseModels/SalesInvoice/SalesInvoice';
import { SalesInvoiceItem } from './baseModels/SalesInvoiceItem/SalesInvoiceItem';
+import { SalesQuote } from './baseModels/SalesQuote/SalesQuote';
+import { SalesQuoteItem } from './baseModels/SalesQuoteItem/SalesQuoteItem';
import { SetupWizard } from './baseModels/SetupWizard/SetupWizard';
import { Tax } from './baseModels/Tax/Tax';
import { TaxSummary } from './baseModels/TaxSummary/TaxSummary';
@@ -61,6 +63,8 @@ export const models = {
PurchaseInvoiceItem,
SalesInvoice,
SalesInvoiceItem,
+ SalesQuote,
+ SalesQuoteItem,
SerialNumber,
SetupWizard,
PrintTemplate,
diff --git a/models/types.ts b/models/types.ts
index b3fdf7307..3a83e4bf7 100644
--- a/models/types.ts
+++ b/models/types.ts
@@ -27,6 +27,8 @@ export enum ModelNameEnum {
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
SalesInvoice = 'SalesInvoice',
SalesInvoiceItem = 'SalesInvoiceItem',
+ SalesQuote = 'SalesQuote',
+ SalesQuoteItem = 'SalesQuoteItem',
SerialNumber = 'SerialNumber',
SetupWizard = 'SetupWizard',
Tax = 'Tax',
diff --git a/schemas/app/Defaults.json b/schemas/app/Defaults.json
index ba9201347..714253cb9 100644
--- a/schemas/app/Defaults.json
+++ b/schemas/app/Defaults.json
@@ -92,6 +92,14 @@
"create": true,
"section": "Number Series"
},
+ {
+ "fieldname": "salesQuoteNumberSeries",
+ "label": "Sales Quote Number Series",
+ "fieldtype": "Link",
+ "target": "NumberSeries",
+ "create": true,
+ "section": "Number Series"
+ },
{
"fieldname": "salesInvoiceTerms",
"label": "Sales Invoice Terms",
@@ -116,6 +124,13 @@
"fieldtype": "Text",
"section": "Terms"
},
+ {
+ "fieldname": "salesQuotePrintTemplate",
+ "label": "Sales Quote Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
{
"fieldname": "salesInvoicePrintTemplate",
"label": "Sales Invoice Print Template",
diff --git a/schemas/app/NumberSeries.json b/schemas/app/NumberSeries.json
index 497b2cc2e..0526e5872 100644
--- a/schemas/app/NumberSeries.json
+++ b/schemas/app/NumberSeries.json
@@ -35,6 +35,10 @@
"value": "SalesInvoice",
"label": "Sales Invoice"
},
+ {
+ "value": "SalesQuote",
+ "label": "Sales Quote"
+ },
{
"value": "PurchaseInvoice",
"label": "Purchase Invoice"
diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json
index d520d0e5c..84e0dfd0d 100644
--- a/schemas/app/SalesInvoice.json
+++ b/schemas/app/SalesInvoice.json
@@ -31,6 +31,14 @@
"target": "Shipment",
"section": "References"
},
+ {
+ "fieldname": "quote",
+ "label": "Quote Reference",
+ "fieldtype": "Link",
+ "target": "SalesQuote",
+ "section": "References",
+ "required": false
+ },
{
"fieldname": "makeAutoStockTransfer",
"label": "Make Shipment On Submit",
diff --git a/schemas/app/SalesQuote.json b/schemas/app/SalesQuote.json
new file mode 100644
index 000000000..9f5248453
--- /dev/null
+++ b/schemas/app/SalesQuote.json
@@ -0,0 +1,46 @@
+{
+ "name": "SalesQuote",
+ "label": "Quote",
+ "extends": "Invoice",
+ "naming": "numberSeries",
+ "showTitle": true,
+ "fields": [
+ {
+ "fieldname": "numberSeries",
+ "label": "Number Series",
+ "fieldtype": "Link",
+ "target": "NumberSeries",
+ "create": true,
+ "required": true,
+ "default": "SQUOT-",
+ "section": "Default"
+ },
+ {
+ "fieldname": "party",
+ "label": "Customer",
+ "fieldtype": "Link",
+ "target": "Party",
+ "create": true,
+ "required": true,
+ "section": "Default"
+ },
+ {
+ "fieldname": "items",
+ "label": "Items",
+ "fieldtype": "Table",
+ "target": "SalesQuoteItem",
+ "required": true,
+ "edit": true,
+ "section": "Items"
+ }
+ ],
+ "keywordFields": ["name", "party"],
+ "removeFields": [
+ "account",
+ "stockNotTransferred",
+ "backReference",
+ "makeAutoStockTransfer",
+ "returnAgainst",
+ "isReturned"
+ ]
+}
diff --git a/schemas/app/SalesQuoteItem.json b/schemas/app/SalesQuoteItem.json
new file mode 100644
index 000000000..41231a8dd
--- /dev/null
+++ b/schemas/app/SalesQuoteItem.json
@@ -0,0 +1,5 @@
+{
+ "name": "SalesQuoteItem",
+ "label": "Sales Quote Item",
+ "extends": "InvoiceItem"
+}
diff --git a/schemas/schemas.ts b/schemas/schemas.ts
index 1e92f56c8..54f4a81ab 100644
--- a/schemas/schemas.ts
+++ b/schemas/schemas.ts
@@ -25,6 +25,8 @@ import PurchaseInvoice from './app/PurchaseInvoice.json';
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
import SalesInvoice from './app/SalesInvoice.json';
import SalesInvoiceItem from './app/SalesInvoiceItem.json';
+import SalesQuote from './app/SalesQuote.json';
+import SalesQuoteItem from './app/SalesQuoteItem.json';
import SetupWizard from './app/SetupWizard.json';
import Tax from './app/Tax.json';
import TaxDetail from './app/TaxDetail.json';
@@ -108,10 +110,12 @@ export const appSchemas: Schema[] | SchemaStub[] = [
Invoice as Schema,
SalesInvoice as Schema,
PurchaseInvoice as Schema,
+ SalesQuote as Schema,
InvoiceItem as Schema,
SalesInvoiceItem as SchemaStub,
PurchaseInvoiceItem as SchemaStub,
+ SalesQuoteItem as SchemaStub,
PriceList as Schema,
PriceListItem as SchemaStub,
diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue
index 581eb87a6..2793958b0 100644
--- a/src/components/Sidebar.vue
+++ b/src/components/Sidebar.vue
@@ -150,6 +150,7 @@
v-if="showDevMode"
class="text-xs text-gray-500 select-none cursor-pointer"
@click="showDevMode = false"
+ title="Open dev tools with Ctrl+Shift+I"
>
dev mode
diff --git a/src/pages/TemplateBuilder/SetType.vue b/src/pages/TemplateBuilder/SetType.vue
new file mode 100644
index 000000000..8b5f142ff
--- /dev/null
+++ b/src/pages/TemplateBuilder/SetType.vue
@@ -0,0 +1,67 @@
+
+
+
+
diff --git a/src/pages/TemplateBuilder/TemplateBuilder.vue b/src/pages/TemplateBuilder/TemplateBuilder.vue
index 6d09ca000..6737ce365 100644
--- a/src/pages/TemplateBuilder/TemplateBuilder.vue
+++ b/src/pages/TemplateBuilder/TemplateBuilder.vue
@@ -213,6 +213,13 @@
>
+
+
+