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..879bd641 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -71,7 +71,11 @@ 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 +497,7 @@ export abstract class Invoice extends Transactional { } async _updateIsItemsReturned() { - if (!this.isReturn || !this.returnAgainst) { + if (!this.isReturn || !this.returnAgainst || this.isQuote) { return; } @@ -515,7 +519,7 @@ export abstract class Invoice extends Transactional { } async _validateHasLinkedReturnInvoices() { - if (!this.name || this.isReturn) { + if (!this.name || this.isReturn || this.isQuote) { return; } @@ -685,6 +689,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..95560849 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -47,7 +47,7 @@ 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..8ac10b3f --- /dev/null +++ b/models/baseModels/SalesQuote/SalesQuote.ts @@ -0,0 +1,65 @@ +import { Fyo } from 'fyo'; +import { Action, ListViewSettings } from 'fyo/model/types'; +import { LedgerPosting } from 'models/Transactional/LedgerPosting'; +import { ModelNameEnum } from 'models/types'; +import { getQuoteActions, getTransactionStatusColumn } from '../../helpers'; +import { Invoice } from '../Invoice/Invoice'; +import { SalesQuoteItem } from '../SalesQuoteItem/SalesQuoteItem'; + +export class SalesQuote extends Invoice { + items?: SalesQuoteItem[]; + + 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 = { + ...this, + 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 + }); + } + + 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..4e0ec45c 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -15,6 +15,15 @@ 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 +76,7 @@ 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 +86,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(); + let 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..2d3f7540 --- /dev/null +++ b/schemas/app/SalesQuote.json @@ -0,0 +1,58 @@ +{ + "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": "account", + "drop": true + }, + { + "fieldname": "stockNotTransferred", + "drop": true + }, + { + "fieldname": "backReference", + "drop": true + }, + { + "fieldname": "makeAutoStockTransfer", + "drop": true + }, + { + "fieldname": "returnAgainst", + "drop": true + }, + { + "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"] +} 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/index.ts b/schemas/index.ts index c7c2f0b8..f5e15b02 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -197,7 +197,11 @@ function getCombined( const combined = Object.assign(abstractSchema, extendingSchema); for (const fieldname in extendingFields) { - abstractFields[fieldname] = extendingFields[fieldname]; + if (extendingFields[fieldname].drop) { + delete abstractFields[fieldname] + } else { + abstractFields[fieldname] = extendingFields[fieldname]; + } } combined.fields = getListFromMap(abstractFields); 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..dd70df3d --- /dev/null +++ b/src/pages/TemplateBuilder/SetType.vue @@ -0,0 +1,71 @@ + + 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 @@ > + + +