diff --git a/backend/patches/updateSchemas.ts b/backend/patches/updateSchemas.ts index e883de26..1ad11bae 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 2515d35e..d590193c 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 04679097..d89d83a0 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 9c39993d..6c9371a1 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 58750643..145401be 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 00000000..2c23731d --- /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 00000000..a2f2133c --- /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 19f32bda..c5a25f49 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 51c1c672..77edb626 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 b3fdf730..3a83e4bf 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 ba920134..714253cb 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 497b2cc2..0526e587 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 d520d0e5..84e0dfd0 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 00000000..9f524845 --- /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 00000000..41231a8d --- /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 1e92f56c..54f4a81a 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 581eb87a..2793958b 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 00000000..8b5f142f --- /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 6d09ca00..6737ce36 100644 --- a/src/pages/TemplateBuilder/TemplateBuilder.vue +++ b/src/pages/TemplateBuilder/TemplateBuilder.vue @@ -213,6 +213,13 @@ > + + +