diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index c089448f..66797c92 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -12,7 +12,7 @@ import { OptionField, RawValue, Schema, - TargetField, + TargetField } from 'schemas/types'; import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils'; import { markRaw } from 'vue'; @@ -23,7 +23,7 @@ import { getMissingMandatoryMessage, getPreDefaultValues, setChildDocIdx, - shouldApplyFormula, + shouldApplyFormula } from './helpers'; import { setName } from './naming'; import { @@ -41,7 +41,7 @@ import { ReadOnlyMap, RequiredMap, TreeViewSettings, - ValidationMap, + ValidationMap } from './types'; import { validateOptions, validateRequired } from './validationFunction'; @@ -186,7 +186,8 @@ export class Doc extends Observable { // set value and trigger change async set( fieldname: string | DocValueMap, - value?: DocValue | Doc[] | DocValueMap[] + value?: DocValue | Doc[] | DocValueMap[], + retriggerChildDocApplyChange: boolean = false ): Promise { if (typeof fieldname === 'object') { return await this.setMultiple(fieldname as DocValueMap); @@ -216,7 +217,7 @@ export class Doc extends Observable { await this._applyChange(fieldname); await this.parentdoc._applyChange(this.parentFieldname as string); } else { - await this._applyChange(fieldname); + await this._applyChange(fieldname, retriggerChildDocApplyChange); } return true; @@ -259,8 +260,11 @@ export class Doc extends Observable { return !areDocValuesEqual(currentValue as DocValue, value as DocValue); } - async _applyChange(fieldname: string): Promise { - await this._applyFormula(fieldname); + async _applyChange( + fieldname: string, + retriggerChildDocApplyChange?: boolean + ): Promise { + await this._applyFormula(fieldname, retriggerChildDocApplyChange); await this.trigger('change', { doc: this, changed: fieldname, @@ -616,37 +620,62 @@ export class Doc extends Observable { } } - async _applyFormula(fieldname?: string): Promise { + async _applyFormula( + fieldname?: string, + retriggerChildDocApplyChange?: boolean + ): Promise { const doc = this; - let changed = false; + let changed = await this._callAllTableFieldsApplyFormula(fieldname); + changed = (await this._applyFormulaForFields(doc, fieldname)) || changed; - const childDocs = this.tableFields - .map((f) => (this.get(f.fieldname) as Doc[]) ?? []) - .flat(); - - // children - for (const row of childDocs) { - changed ||= (await row?._applyFormula()) ?? false; + if (changed && retriggerChildDocApplyChange) { + await this._callAllTableFieldsApplyFormula(fieldname); + await this._applyFormulaForFields(doc, fieldname); } - // parent or child row + return changed; + } + + async _callAllTableFieldsApplyFormula( + changedFieldname?: string + ): Promise { + let changed = false; + + for (const { fieldname } of this.tableFields) { + const childDocs = this.get(fieldname) as Doc[]; + if (!childDocs) { + continue; + } + + changed = + (await this._callChildDocApplyFormula(childDocs, changedFieldname)) || + changed; + } + + return changed; + } + + async _callChildDocApplyFormula( + childDocs: Doc[], + fieldname?: string + ): Promise { + let changed: boolean = false; + for (const childDoc of childDocs) { + if (!childDoc._applyFormula) { + continue; + } + + changed = (await childDoc._applyFormula(fieldname)) || changed; + } + + return changed; + } + + async _applyFormulaForFields(doc: Doc, fieldname?: string) { const formulaFields = Object.keys(this.formulas).map( (fn) => this.fieldMap[fn] ); - changed ||= await this._applyFormulaForFields( - formulaFields, - doc, - fieldname - ); - return changed; - } - - async _applyFormulaForFields( - formulaFields: Field[], - doc: Doc, - fieldname?: string - ) { let changed = false; for (const field of formulaFields) { const shouldApply = shouldApplyFormula(field, doc, fieldname); @@ -662,7 +691,7 @@ export class Doc extends Observable { } doc[field.fieldname] = newVal; - changed = true; + changed ||= true; } return changed; diff --git a/fyo/models/SystemSettings.ts b/fyo/models/SystemSettings.ts index 7eb5c327..6d6f1d74 100644 --- a/fyo/models/SystemSettings.ts +++ b/fyo/models/SystemSettings.ts @@ -7,6 +7,16 @@ import { SelectOption } from 'schemas/types'; import { getCountryInfo } from 'utils/misc'; export default class SystemSettings extends Doc { + dateFormat?: string; + locale?: string; + displayPrecision?: number; + internalPrecision?: number; + hideGetStarted?: boolean; + countryCode?: string; + currency?: string; + version?: string; + instanceId?: string; + validations: ValidationMap = { async displayPrecision(value: DocValue) { if ((value as number) >= 0 && (value as number) <= 9) { diff --git a/fyo/utils/format.ts b/fyo/utils/format.ts index 6abd0f47..8e6be3b5 100644 --- a/fyo/utils/format.ts +++ b/fyo/utils/format.ts @@ -70,7 +70,7 @@ function formatCurrency( doc: Doc | null, fyo: Fyo ): string { - const currency = getCurrency(field, doc, fyo); + const currency = getCurrency(value as Money, field, doc, fyo); let valueString; try { @@ -128,7 +128,20 @@ function getNumberFormatter(fyo: Fyo) { })); } -function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string { +function getCurrency( + value: Money, + field: Field, + doc: Doc | null, + fyo: Fyo +): string { + const currency = value?.getCurrency?.(); + const defaultCurrency = + fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY; + + if (currency && currency !== defaultCurrency) { + return currency; + } + let getCurrency = doc?.getCurrencies?.[field.fieldname]; if (getCurrency !== undefined) { return getCurrency(); @@ -139,7 +152,7 @@ function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string { return getCurrency(); } - return (fyo.singles.SystemSettings?.currency as string) ?? DEFAULT_CURRENCY; + return defaultCurrency; } function getField(df: string | Field): Field { diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 8a7cabff..71a939c8 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -1,11 +1,20 @@ -import { DocValue } from 'fyo/core/types'; +import { Fyo } from 'fyo'; +import { DocValue, DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { DefaultMap, FiltersMap, FormulaMap, HiddenMap } from 'fyo/model/types'; +import { + CurrenciesMap, + DefaultMap, + FiltersMap, + FormulaMap, + HiddenMap +} from 'fyo/model/types'; +import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { ValidationError } from 'fyo/utils/errors'; import { getExchangeRate } from 'models/helpers'; import { Transactional } from 'models/Transactional/Transactional'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; +import { FieldTypeEnum, Schema } from 'schemas/types'; import { getIsNullOrUndef } from 'utils'; import { InvoiceItem } from '../InvoiceItem/InvoiceItem'; import { Party } from '../Party/Party'; @@ -42,6 +51,23 @@ export abstract class Invoice extends Transactional { return !!this.fyo.singles?.AccountingSettings?.enableDiscounting; } + get isMultiCurrency() { + if (!this.currency) { + return false; + } + + return this.fyo.singles.SystemSettings!.currency !== this.currency; + } + + get companyCurrency() { + return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY; + } + + constructor(schema: Schema, data: DocValueMap, fyo: Fyo) { + super(schema, data, fyo); + this._setGetCurrencies(); + } + async validate() { await super.validate(); if ( @@ -126,10 +152,12 @@ export abstract class Invoice extends Transactional { if (this.currency === currency) { return 1.0; } - return await getExchangeRate({ + const exchangeRate = await getExchangeRate({ fromCurrency: this.currency!, toCurrency: currency as string, }); + + return parseFloat(exchangeRate.toFixed(2)); } async getTaxSummary() { @@ -139,7 +167,6 @@ export abstract class Invoice extends Transactional { account: string; rate: number; amount: Money; - baseAmount: Money; [key: string]: DocValue; } > = {}; @@ -157,7 +184,6 @@ export abstract class Invoice extends Transactional { account, rate, amount: this.fyo.pesa(0), - baseAmount: this.fyo.pesa(0), }; let amount = item.amount!; @@ -172,9 +198,7 @@ export abstract class Invoice extends Transactional { return Object.keys(taxes) .map((account) => { - const tax = taxes[account]; - tax.baseAmount = tax.amount.mul(this.exchangeRate!); - return tax; + return taxes[account]; }) .filter((tax) => !tax.amount.isZero()); } @@ -285,15 +309,28 @@ export abstract class Invoice extends Transactional { }, dependsOn: ['party'], }, - exchangeRate: { formula: async () => await this.getExchangeRate() }, - netTotal: { formula: async () => this.getSum('items', 'amount', false) }, - baseNetTotal: { - formula: async () => this.netTotal!.mul(this.exchangeRate!), + exchangeRate: { + formula: async () => { + if ( + this.currency === + (this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY) + ) { + return 1; + } + + if (this.exchangeRate && this.exchangeRate !== 1) { + return this.exchangeRate; + } + + return await this.getExchangeRate(); + }, }, + netTotal: { formula: async () => this.getSum('items', 'amount', false) }, taxes: { formula: async () => await this.getTaxSummary() }, grandTotal: { formula: async () => await this.getGrandTotal() }, baseGrandTotal: { - formula: async () => (this.grandTotal as Money).mul(this.exchangeRate!), + formula: async () => + (this.grandTotal as Money).mul(this.exchangeRate! ?? 1), }, outstandingAmount: { formula: async () => { @@ -345,4 +382,25 @@ export abstract class Invoice extends Transactional { role: doc.isSales ? 'Customer' : 'Supplier', }), }; + + getCurrencies: CurrenciesMap = { + baseGrandTotal: () => this.companyCurrency, + outstandingAmount: () => this.companyCurrency, + }; + _getCurrency() { + if (this.exchangeRate === 1) { + return this.companyCurrency; + } + + return this.currency ?? DEFAULT_CURRENCY; + } + _setGetCurrencies() { + const currencyFields = this.schema.fields.filter( + ({ fieldtype }) => fieldtype === FieldTypeEnum.Currency + ); + + for (const { fieldname } of currencyFields) { + this.getCurrencies[fieldname] ??= this._getCurrency.bind(this); + } + } } diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 58fa4606..8d6009e5 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -1,21 +1,23 @@ -import { DocValue } from 'fyo/core/types'; +import { Fyo } from 'fyo'; +import { DocValue, DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; import { + CurrenciesMap, FiltersMap, FormulaMap, HiddenMap, ValidationMap } from 'fyo/model/types'; +import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { ValidationError } from 'fyo/utils/errors'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; +import { FieldTypeEnum, Schema } from 'schemas/types'; import { Invoice } from '../Invoice/Invoice'; export abstract class InvoiceItem extends Doc { account?: string; amount?: Money; - baseAmount?: Money; - exchangeRate?: number; parentdoc?: Invoice; rate?: Money; quantity?: number; @@ -39,6 +41,23 @@ export abstract class InvoiceItem extends Doc { return !!this.fyo.singles?.AccountingSettings?.enableDiscounting; } + get currency() { + return this.parentdoc?.currency ?? DEFAULT_CURRENCY; + } + + get exchangeRate() { + return this.parentdoc?.exchangeRate ?? 1; + } + + get isMultiCurrency() { + return this.parentdoc?.isMultiCurrency ?? false; + } + + constructor(schema: Schema, data: DocValueMap, fyo: Fyo) { + super(schema, data, fyo); + this._setGetCurrencies(); + } + async getTotalTaxRate(): Promise { if (!this.tax) { return 0; @@ -73,7 +92,7 @@ export abstract class InvoiceItem extends Doc { fieldname !== 'itemTaxedTotal' && fieldname !== 'itemDiscountedTotal' ) { - return rate ?? this.fyo.pesa(0); + return rate?.div(this.exchangeRate) ?? this.fyo.pesa(0); } const quantity = this.quantity ?? 0; @@ -102,17 +121,14 @@ export abstract class InvoiceItem extends Doc { return rateFromTotals ?? rate ?? this.fyo.pesa(0); }, dependsOn: [ + 'party', + 'exchangeRate', 'item', 'itemTaxedTotal', 'itemDiscountedTotal', 'setItemDiscountAmount', ], }, - baseRate: { - formula: () => - (this.rate as Money).mul(this.parentdoc!.exchangeRate as number), - dependsOn: ['item', 'rate'], - }, quantity: { formula: async () => { if (!this.item) { @@ -157,11 +173,6 @@ export abstract class InvoiceItem extends Doc { formula: () => (this.rate as Money).mul(this.quantity as number), dependsOn: ['item', 'rate', 'quantity'], }, - baseAmount: { - formula: () => - (this.amount as Money).mul(this.parentdoc!.exchangeRate as number), - dependsOn: ['item', 'amount', 'rate', 'quantity'], - }, hsnCode: { formula: async () => await this.fyo.getValue('Item', this.item as string, 'hsnCode'), @@ -338,6 +349,24 @@ export abstract class InvoiceItem extends Doc { return { for: doc.isSales ? 'Sales' : 'Purchases' }; }, }; + + getCurrencies: CurrenciesMap = {}; + _getCurrency() { + if (this.exchangeRate === 1) { + return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY; + } + + return this.currency; + } + _setGetCurrencies() { + const currencyFields = this.schema.fields.filter( + ({ fieldtype }) => fieldtype === FieldTypeEnum.Currency + ); + + for (const { fieldname } of currencyFields) { + this.getCurrencies[fieldname] ??= this._getCurrency.bind(this); + } + } } function getDiscountedTotalBeforeTaxation( diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index 6dbeafca..db500e4f 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -87,7 +87,11 @@ export class Party extends Doc { dependsOn: ['role'], }, currency: { - formula: async () => this.fyo.singles.SystemSettings!.currency as string, + formula: async () => { + if (!this.currency) { + return this.fyo.singles.SystemSettings!.currency as string; + } + }, }, }; diff --git a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts index 47caf0a8..38f447c9 100644 --- a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts +++ b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts @@ -10,16 +10,17 @@ export class PurchaseInvoice extends Invoice { items?: PurchaseInvoiceItem[]; async getPosting() { + const exchangeRate = this.exchangeRate ?? 1; const posting: LedgerPosting = new LedgerPosting(this, this.fyo); await posting.credit(this.account!, this.baseGrandTotal!); for (const item of this.items!) { - await posting.debit(item.account!, item.baseAmount!); + await posting.debit(item.account!, item.amount!.mul(exchangeRate)); } if (this.taxes) { for (const tax of this.taxes) { - await posting.debit(tax.account!, tax.baseAmount!); + await posting.debit(tax.account!, tax.amount!.mul(exchangeRate)); } } @@ -27,7 +28,7 @@ export class PurchaseInvoice extends Invoice { const discountAccount = this.fyo.singles.AccountingSettings ?.discountAccount as string | undefined; if (discountAccount && discountAmount.isPositive()) { - await posting.credit(discountAccount, discountAmount); + await posting.credit(discountAccount, discountAmount.mul(exchangeRate)); } await posting.makeRoundOffEntry(); @@ -46,7 +47,7 @@ export class PurchaseInvoice extends Invoice { getTransactionStatusColumn(), 'party', 'date', - 'grandTotal', + 'baseGrandTotal', 'outstandingAmount', ], }; diff --git a/models/baseModels/SalesInvoice/SalesInvoice.ts b/models/baseModels/SalesInvoice/SalesInvoice.ts index d50a6345..5fd7b0d0 100644 --- a/models/baseModels/SalesInvoice/SalesInvoice.ts +++ b/models/baseModels/SalesInvoice/SalesInvoice.ts @@ -10,16 +10,17 @@ export class SalesInvoice extends Invoice { items?: SalesInvoiceItem[]; async getPosting() { + const exchangeRate = this.exchangeRate ?? 1; const posting: LedgerPosting = new LedgerPosting(this, this.fyo); await posting.debit(this.account!, this.baseGrandTotal!); for (const item of this.items!) { - await posting.credit(item.account!, item.baseAmount!); + await posting.credit(item.account!, item.amount!.mul(exchangeRate)); } if (this.taxes) { for (const tax of this.taxes!) { - await posting.credit(tax.account!, tax.baseAmount!); + await posting.credit(tax.account!, tax.amount!.mul(exchangeRate)); } } @@ -27,7 +28,7 @@ export class SalesInvoice extends Invoice { const discountAccount = this.fyo.singles.AccountingSettings ?.discountAccount as string | undefined; if (discountAccount && discountAmount.isPositive()) { - await posting.debit(discountAccount, discountAmount); + await posting.debit(discountAccount, discountAmount.mul(exchangeRate)); } await posting.makeRoundOffEntry(); @@ -46,7 +47,7 @@ export class SalesInvoice extends Invoice { getTransactionStatusColumn(), 'party', 'date', - 'grandTotal', + 'baseGrandTotal', 'outstandingAmount', ], }; diff --git a/models/baseModels/TaxSummary/TaxSummary.ts b/models/baseModels/TaxSummary/TaxSummary.ts index 7ca232ba..bb9438e4 100644 --- a/models/baseModels/TaxSummary/TaxSummary.ts +++ b/models/baseModels/TaxSummary/TaxSummary.ts @@ -1,21 +1,48 @@ +import { Fyo } from 'fyo'; +import { DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FormulaMap } from 'fyo/model/types'; +import { CurrenciesMap } from 'fyo/model/types'; +import { DEFAULT_CURRENCY } from 'fyo/utils/consts'; import { Money } from 'pesa'; +import { FieldTypeEnum, Schema } from 'schemas/types'; +import { Invoice } from '../Invoice/Invoice'; export class TaxSummary extends Doc { account?: string; rate?: number; amount?: Money; - baseAmount?: Money; + parentdoc?: Invoice; - formulas: FormulaMap = { - baseAmount: { - formula: async () => { - const amount = this.amount as Money; - const exchangeRate = (this.parentdoc?.exchangeRate ?? 1) as number; - return amount.mul(exchangeRate); - }, - dependsOn: ['amount'], - }, - }; + get exchangeRate() { + return this.parentdoc?.exchangeRate ?? 1; + } + + get currency() { + return this.parentdoc?.currency ?? DEFAULT_CURRENCY; + } + + constructor(schema: Schema, data: DocValueMap, fyo: Fyo) { + super(schema, data, fyo); + this._setGetCurrencies(); + } + + getCurrencies: CurrenciesMap = {}; + _getCurrency() { + if (this.exchangeRate === 1) { + return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY; + } + + return this.currency; + } + _setGetCurrencies() { + const currencyFields = this.schema.fields.filter( + ({ fieldtype }) => fieldtype === FieldTypeEnum.Currency + ); + + const getCurrency = this._getCurrency.bind(this); + + for (const { fieldname } of currencyFields) { + this.getCurrencies[fieldname] ??= getCurrency; + } + } } diff --git a/models/helpers.ts b/models/helpers.ts index 09d84d48..e78580ef 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -1,13 +1,12 @@ import { Fyo, t } from 'fyo'; import { Doc } from 'fyo/model/doc'; import { Action, ColumnConfig, DocStatus, RenderData } from 'fyo/model/types'; -import { NotFoundError } from 'fyo/utils/errors'; import { DateTime } from 'luxon'; import { Money } from 'pesa'; import { Router } from 'vue-router'; import { AccountRootType, - AccountRootTypeEnum, + AccountRootTypeEnum } from './baseModels/Account/types'; import { InvoiceStatus, ModelNameEnum } from './types'; @@ -196,14 +195,12 @@ export async function getExchangeRate({ toCurrency: string; date?: string; }) { - if (!date) { - date = DateTime.local().toISODate(); + if (!fetch) { + return 1; } - if (!fromCurrency || !toCurrency) { - throw new NotFoundError( - 'Please provide `fromCurrency` and `toCurrency` to get exchange rate.' - ); + if (!date) { + date = DateTime.local().toISODate(); } const cacheKey = `currencyExchangeRate:${date}:${fromCurrency}:${toCurrency}`; @@ -215,26 +212,23 @@ export async function getExchangeRate({ ); } - if (!exchangeRate && fetch) { - try { - const res = await fetch( - ` https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}` - ); - const data = await res.json(); - exchangeRate = data.rates[toCurrency]; + if (exchangeRate && exchangeRate !== 1) { + return exchangeRate; + } - if (localStorage) { - localStorage.setItem(cacheKey, String(exchangeRate)); - } - } catch (error) { - console.error(error); - throw new NotFoundError( - `Could not fetch exchange rate for ${fromCurrency} -> ${toCurrency}`, - false - ); - } - } else { - exchangeRate = 1; + try { + const res = await fetch( + `https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}` + ); + const data = await res.json(); + exchangeRate = data.rates[toCurrency]; + } catch (error) { + console.error(error); + exchangeRate ??= 1; + } + + if (localStorage) { + localStorage.setItem(cacheKey, String(exchangeRate)); } return exchangeRate; @@ -256,3 +250,6 @@ export function isCredit(rootType: AccountRootType) { return true; } } + +// @ts-ignore +window.gex = getExchangeRate; diff --git a/reports/GoodsAndServiceTax/gstExporter.ts b/reports/GoodsAndServiceTax/gstExporter.ts index 42011683..01849fdd 100644 --- a/reports/GoodsAndServiceTax/gstExporter.ts +++ b/reports/GoodsAndServiceTax/gstExporter.ts @@ -218,6 +218,11 @@ async function generateB2bData(report: BaseGSTR): Promise { ? ModelNameEnum.SalesInvoiceItem : ModelNameEnum.PurchaseInvoiceItem; + const parentSchemaName = + report.gstrType === 'GSTR-1' + ? ModelNameEnum.SalesInvoice + : ModelNameEnum.PurchaseInvoice; + for (const row of report.gstrRows ?? []) { const invRecord: B2BInvRecord = { inum: row.invNo, @@ -229,20 +234,29 @@ async function generateB2bData(report: BaseGSTR): Promise { itms: [], }; + const exchangeRate = ( + await fyo.db.getAllRaw(parentSchemaName, { + fields: ['exchangeRate'], + filters: { name: invRecord.inum }, + }) + )[0].exchangeRate as number; + const items = await fyo.db.getAllRaw(schemaName, { - fields: ['baseAmount', 'tax', 'hsnCode'], - filters: { parent: invRecord.inum as string }, + fields: ['amount', 'tax', 'hsnCode'], + filters: { parent: invRecord.inum }, }); items.forEach((item) => { const hsnCode = item.hsnCode as number; const tax = item.tax as string; - const baseAmount = (item.baseAmount ?? 0) as string; + const baseAmount = fyo + .pesa((item.amount as string) ?? 0) + .mul(exchangeRate); const itemRecord: B2BItmRecord = { num: hsnCode, itm_det: { - txval: fyo.pesa(baseAmount).float, + txval: baseAmount.float, rt: GST[tax], csamt: 0, camt: fyo @@ -292,6 +306,11 @@ async function generateB2clData( ? ModelNameEnum.SalesInvoiceItem : ModelNameEnum.PurchaseInvoiceItem; + const parentSchemaName = + report.gstrType === 'GSTR-1' + ? ModelNameEnum.SalesInvoice + : ModelNameEnum.PurchaseInvoice; + for (const row of report.gstrRows ?? []) { const invRecord: B2CLInvRecord = { inum: row.invNo, @@ -300,20 +319,29 @@ async function generateB2clData( itms: [], }; + const exchangeRate = ( + await fyo.db.getAllRaw(parentSchemaName, { + fields: ['exchangeRate'], + filters: { name: invRecord.inum }, + }) + )[0].exchangeRate as number; + const items = await fyo.db.getAllRaw(schemaName, { - fields: ['hsnCode', 'tax', 'baseAmount'], + fields: ['amount', 'tax', 'hsnCode'], filters: { parent: invRecord.inum }, }); items.forEach((item) => { const hsnCode = item.hsnCode as number; const tax = item.tax as string; - const baseAmount = (item.baseAmount ?? 0) as string; + const baseAmount = fyo + .pesa((item.amount as string) ?? 0) + .mul(exchangeRate); const itemRecord: B2CLItmRecord = { num: hsnCode, itm_det: { - txval: fyo.pesa(baseAmount).float, + txval: baseAmount.float, rt: GST[tax] ?? 0, csamt: 0, iamt: fyo diff --git a/schemas/app/Invoice.json b/schemas/app/Invoice.json new file mode 100644 index 00000000..6e281fbb --- /dev/null +++ b/schemas/app/Invoice.json @@ -0,0 +1,131 @@ +{ + "name": "Invoice", + "label": "Invoice", + "isAbstract": true, + "isSingle": false, + "isChild": false, + "isSubmittable": true, + "fields": [ + { + "label": "Invoice No", + "fieldname": "name", + "fieldtype": "Data", + "required": true, + "readOnly": true + }, + { + "fieldname": "date", + "label": "Date", + "fieldtype": "Date", + "required": true + }, + { + "fieldname": "party", + "label": "Party", + "fieldtype": "Link", + "target": "Party", + "create": true, + "required": true + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account", + "create": true, + "required": true + }, + { + "fieldname": "currency", + "label": "Customer Currency", + "fieldtype": "Link", + "target": "Currency" + }, + { + "fieldname": "exchangeRate", + "label": "Exchange Rate", + "fieldtype": "Float", + "default": 1, + "readOnly": true + }, + { + "fieldname": "netTotal", + "label": "Net Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "taxes", + "label": "Taxes", + "fieldtype": "Table", + "target": "TaxSummary", + "readOnly": true + }, + { + "fieldname": "grandTotal", + "label": "Grand Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseGrandTotal", + "label": "Base Grand Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "setDiscountAmount", + "label": "Set Discount Amount", + "fieldtype": "Check", + "default": false + }, + { + "fieldname": "discountAmount", + "label": "Discount Amount", + "fieldtype": "Currency", + "readOnly": false + }, + { + "fieldname": "discountPercent", + "label": "Discount Percent", + "fieldtype": "Float", + "readOnly": false + }, + { + "fieldname": "discountAfterTax", + "label": "Discount After Tax", + "fieldtype": "Check", + "default": false, + "readOnly": false + }, + { + "fieldname": "entryCurrency", + "label": "Entry Currency", + "fieldtype": "Select", + "options": [ + { + "value": "Party", + "label": "Party" + }, + { + "value": "Company", + "label": "Company" + } + ], + "default": "Party" + }, + { + "fieldname": "terms", + "label": "Notes", + "placeholder": "Add invoice terms", + "fieldtype": "Text" + } + ], + "keywordFields": ["name", "party"] +} diff --git a/schemas/app/InvoiceItem.json b/schemas/app/InvoiceItem.json new file mode 100644 index 00000000..e2130a4f --- /dev/null +++ b/schemas/app/InvoiceItem.json @@ -0,0 +1,109 @@ +{ + "name": "InvoiceItem", + "label": "Invoice Item", + "isAbstract": true, + "isChild": true, + "fields": [ + { + "fieldname": "item", + "label": "Item", + "fieldtype": "Link", + "target": "Item", + "create": true, + "required": true + }, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Text" + }, + { + "fieldname": "quantity", + "label": "Quantity", + "fieldtype": "Float", + "required": true, + "default": 1 + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Currency", + "required": true + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "tax", + "label": "Tax", + "fieldtype": "Link", + "create": true, + "target": "Tax" + }, + { + "fieldname": "amount", + "label": "Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "setItemDiscountAmount", + "label": "Set Discount Amount", + "fieldtype": "Check", + "default": false + }, + { + "fieldname": "itemDiscountAmount", + "label": "Discount Amount", + "fieldtype": "Currency", + "readOnly": false + }, + { + "fieldname": "itemDiscountPercent", + "label": "Discount Percent", + "fieldtype": "Float", + "readOnly": false + }, + { + "fieldname": "itemDiscountedTotal", + "label": "Discounted Amount", + "fieldtype": "Currency", + "readOnly": false, + "computed": true + }, + { + "fieldname": "itemTaxedTotal", + "label": "Taxed Amount", + "fieldtype": "Currency", + "readOnly": false, + "computed": true + }, + { + "fieldname": "hsnCode", + "label": "HSN/SAC", + "fieldtype": "Int", + "placeholder": "HSN/SAC Code" + } + ], + "tableFields": ["item", "tax", "quantity", "rate", "amount"], + "keywordFields": ["item", "tax"], + "quickEditFields": [ + "item", + "account", + "description", + "hsnCode", + "tax", + "quantity", + "rate", + "amount", + "setItemDiscountAmount", + "itemDiscountAmount", + "itemDiscountPercent", + "itemDiscountedTotal", + "itemTaxedTotal" + ] +} diff --git a/schemas/app/PurchaseInvoice.json b/schemas/app/PurchaseInvoice.json index d22389ec..d1608f54 100644 --- a/schemas/app/PurchaseInvoice.json +++ b/schemas/app/PurchaseInvoice.json @@ -1,54 +1,10 @@ { "name": "PurchaseInvoice", "label": "Purchase Invoice", + "extends": "Invoice", "naming": "numberSeries", - "isSingle": false, - "isChild": false, - "isSubmittable": true, "showTitle": true, "fields": [ - { - "label": "Bill No", - "fieldname": "name", - "fieldtype": "Data", - "required": true, - "readOnly": true - }, - { - "fieldname": "date", - "label": "Date", - "fieldtype": "Date", - "required": true - }, - { - "fieldname": "party", - "label": "Party", - "fieldtype": "Link", - "target": "Party", - "create": true, - "required": true - }, - { - "fieldname": "account", - "label": "Account", - "fieldtype": "Link", - "target": "Account", - "create": true, - "required": true - }, - { - "fieldname": "currency", - "label": "Supplier Currency", - "fieldtype": "Link", - "target": "Currency", - "hidden": true - }, - { - "fieldname": "exchangeRate", - "label": "Exchange Rate", - "fieldtype": "Float", - "default": 1 - }, { "fieldname": "items", "label": "Items", @@ -57,75 +13,6 @@ "required": true, "edit": true }, - { - "fieldname": "netTotal", - "label": "Net Total", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseNetTotal", - "label": "Net Total (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "taxes", - "label": "Taxes", - "fieldtype": "Table", - "target": "TaxSummary", - "readOnly": true - }, - { - "fieldname": "grandTotal", - "label": "Grand Total", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseGrandTotal", - "label": "Grand Total (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "outstandingAmount", - "label": "Outstanding Amount", - "fieldtype": "Currency", - "readOnly": true, - "filter": true - }, - { - "fieldname": "setDiscountAmount", - "label": "Set Discount Amount", - "fieldtype": "Check", - "default": false - }, - { - "fieldname": "discountAmount", - "label": "Discount Amount", - "fieldtype": "Currency", - "readOnly": false - }, - { - "fieldname": "discountPercent", - "label": "Discount Percent", - "fieldtype": "Float", - "readOnly": false - }, - { - "fieldname": "discountAfterTax", - "label": "Discount After Tax", - "fieldtype": "Check", - "default": false, - "readOnly": false - }, - { - "fieldname": "terms", - "label": "Terms", - "placeholder": "Add invoice terms", - "fieldtype": "Text" - }, { "fieldname": "numberSeries", "label": "Number Series", diff --git a/schemas/app/PurchaseInvoiceItem.json b/schemas/app/PurchaseInvoiceItem.json index e3d91106..c15199e5 100644 --- a/schemas/app/PurchaseInvoiceItem.json +++ b/schemas/app/PurchaseInvoiceItem.json @@ -1,124 +1,5 @@ { "name": "PurchaseInvoiceItem", "label": "Purchase Invoice Item", - "isChild": true, - "fields": [ - { - "fieldname": "item", - "label": "Item", - "fieldtype": "Link", - "target": "Item", - "create": true, - "required": true - }, - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Text", - "hidden": true - }, - { - "fieldname": "quantity", - "label": "Quantity", - "fieldtype": "Float", - "required": true, - "default": 1 - }, - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Currency", - "required": true - }, - { - "fieldname": "baseRate", - "label": "Rate (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "account", - "label": "Account", - "fieldtype": "Link", - "target": "Account", - "create": true, - "required": true, - "readOnly": true - }, - { - "fieldname": "tax", - "label": "Tax", - "fieldtype": "Link", - "create": true, - "target": "Tax" - }, - { - "fieldname": "amount", - "label": "Amount", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseAmount", - "label": "Amount (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "setItemDiscountAmount", - "label": "Set Discount Amount", - "fieldtype": "Check", - "default": false - }, - { - "fieldname": "itemDiscountAmount", - "label": "Discount Amount", - "fieldtype": "Currency", - "readOnly": false - }, - { - "fieldname": "itemDiscountPercent", - "label": "Discount Percent", - "fieldtype": "Float", - "readOnly": false - }, - { - "fieldname": "itemDiscountedTotal", - "label": "Discounted Amount", - "fieldtype": "Currency", - "readOnly": false, - "computed": true - }, - { - "fieldname": "itemTaxedTotal", - "label": "Taxed Amount", - "fieldtype": "Currency", - "readOnly": false, - "computed": true - }, - { - "fieldname": "hsnCode", - "label": "HSN/SAC", - "fieldtype": "Int", - "placeholder": "HSN/SAC Code", - "hidden": true - } - ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"], - "keywordFields": ["item", "tax"], - "quickEditFields": [ - "item", - "account", - "description", - "hsnCode", - "tax", - "quantity", - "rate", - "amount", - "setItemDiscountAmount", - "itemDiscountAmount", - "itemDiscountPercent", - "itemDiscountedTotal", - "itemTaxedTotal" - ] + "extends": "InvoiceItem" } diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index a2228307..931f780a 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -1,53 +1,10 @@ { "name": "SalesInvoice", "label": "Sales Invoice", + "extends": "Invoice", "naming": "numberSeries", - "isSingle": false, - "isChild": false, - "isSubmittable": true, + "showTitle": true, "fields": [ - { - "label": "Invoice No", - "fieldname": "name", - "fieldtype": "Data", - "required": true, - "readOnly": true - }, - { - "fieldname": "date", - "label": "Date", - "fieldtype": "Date", - "required": true - }, - { - "fieldname": "party", - "label": "Party", - "fieldtype": "Link", - "target": "Party", - "create": true, - "required": true - }, - { - "fieldname": "account", - "label": "Account", - "fieldtype": "Link", - "target": "Account", - "create": true, - "required": true - }, - { - "fieldname": "currency", - "label": "Customer Currency", - "fieldtype": "Link", - "target": "Currency" - }, - { - "fieldname": "exchangeRate", - "label": "Exchange Rate", - "fieldtype": "Float", - "default": 1, - "readOnly": true - }, { "fieldname": "items", "label": "Items", @@ -56,75 +13,6 @@ "required": true, "edit": true }, - { - "fieldname": "netTotal", - "label": "Net Total", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseNetTotal", - "label": "Net Total (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "taxes", - "label": "Taxes", - "fieldtype": "Table", - "target": "TaxSummary", - "readOnly": true - }, - { - "fieldname": "grandTotal", - "label": "Grand Total", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseGrandTotal", - "label": "Grand Total (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "outstandingAmount", - "label": "Outstanding Amount", - "fieldtype": "Currency", - "readOnly": true, - "filter": true - }, - { - "fieldname": "setDiscountAmount", - "label": "Set Discount Amount", - "fieldtype": "Check", - "default": false - }, - { - "fieldname": "discountAmount", - "label": "Discount Amount", - "fieldtype": "Currency", - "readOnly": false - }, - { - "fieldname": "discountPercent", - "label": "Discount Percent", - "fieldtype": "Float", - "readOnly": false - }, - { - "fieldname": "discountAfterTax", - "label": "Discount After Tax", - "fieldtype": "Check", - "default": false, - "readOnly": false - }, - { - "fieldname": "terms", - "label": "Notes", - "placeholder": "Add invoice terms", - "fieldtype": "Text" - }, { "fieldname": "numberSeries", "label": "Number Series", diff --git a/schemas/app/SalesInvoiceItem.json b/schemas/app/SalesInvoiceItem.json index ce46b5dd..bff7b705 100644 --- a/schemas/app/SalesInvoiceItem.json +++ b/schemas/app/SalesInvoiceItem.json @@ -1,120 +1,5 @@ { "name": "SalesInvoiceItem", "label": "Sales Invoice Item", - "isChild": true, - "fields": [ - { - "fieldname": "item", - "label": "Item", - "fieldtype": "Link", - "target": "Item", - "create": true, - "required": true - }, - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Text" - }, - { - "fieldname": "quantity", - "label": "Quantity", - "fieldtype": "Float", - "required": true, - "default": 1 - }, - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Currency", - "required": true - }, - { - "fieldname": "baseRate", - "label": "Rate (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "account", - "label": "Account", - "fieldtype": "Link", - "target": "Account", - "required": true - }, - { - "fieldname": "tax", - "label": "Tax", - "fieldtype": "Link", - "create": true, - "target": "Tax" - }, - { - "fieldname": "amount", - "label": "Amount", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "baseAmount", - "label": "Amount (Company Currency)", - "fieldtype": "Currency", - "readOnly": true - }, - { - "fieldname": "setItemDiscountAmount", - "label": "Set Discount Amount", - "fieldtype": "Check", - "default": false - }, - { - "fieldname": "itemDiscountAmount", - "label": "Discount Amount", - "fieldtype": "Currency", - "readOnly": false - }, - { - "fieldname": "itemDiscountPercent", - "label": "Discount Percent", - "fieldtype": "Float", - "readOnly": false - }, - { - "fieldname": "itemDiscountedTotal", - "label": "Discounted Amount", - "fieldtype": "Currency", - "readOnly": false, - "computed": true - }, - { - "fieldname": "itemTaxedTotal", - "label": "Taxed Amount", - "fieldtype": "Currency", - "readOnly": false, - "computed": true - }, - { - "fieldname": "hsnCode", - "label": "HSN/SAC", - "fieldtype": "Int", - "placeholder": "HSN/SAC Code" - } - ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"], - "keywordFields": ["item", "tax"], - "quickEditFields": [ - "item", - "account", - "description", - "hsnCode", - "tax", - "quantity", - "rate", - "amount", - "setItemDiscountAmount", - "itemDiscountAmount", - "itemDiscountPercent", - "itemDiscountedTotal", - "itemTaxedTotal" - ] + "extends": "InvoiceItem" } diff --git a/schemas/app/TaxSummary.json b/schemas/app/TaxSummary.json index 71e23921..2f044605 100644 --- a/schemas/app/TaxSummary.json +++ b/schemas/app/TaxSummary.json @@ -21,12 +21,6 @@ "label": "Amount", "fieldtype": "Currency", "required": true - }, - { - "fieldname": "baseAmount", - "label": "Amount (Company Currency)", - "fieldtype": "Currency", - "readOnly": true } ] } \ No newline at end of file diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 92c33fb8..ba67d038 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -88,6 +88,7 @@ "label": "Currency", "fieldtype": "AutoComplete", "default": "INR", + "readOnly": true, "required": true }, { diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 3fb6cc36..22935db9 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -7,6 +7,8 @@ import Color from './app/Color.json'; import CompanySettings from './app/CompanySettings.json'; import Currency from './app/Currency.json'; import GetStarted from './app/GetStarted.json'; +import Invoice from './app/Invoice.json'; +import InvoiceItem from './app/InvoiceItem.json'; import Item from './app/Item.json'; import JournalEntry from './app/JournalEntry.json'; import JournalEntryAccount from './app/JournalEntryAccount.json'; @@ -73,11 +75,13 @@ export const appSchemas: Schema[] | SchemaStub[] = [ JournalEntry as Schema, JournalEntryAccount as Schema, - PurchaseInvoice as Schema, - PurchaseInvoiceItem as Schema, - + Invoice as Schema, SalesInvoice as Schema, - SalesInvoiceItem as Schema, + PurchaseInvoice as Schema, + + InvoiceItem as Schema, + SalesInvoiceItem as SchemaStub, + PurchaseInvoiceItem as SchemaStub, Tax as Schema, TaxDetail as Schema, diff --git a/src/components/Controls/Base.vue b/src/components/Controls/Base.vue index dbd34c79..dc3ce130 100644 --- a/src/components/Controls/Base.vue +++ b/src/components/Controls/Base.vue @@ -151,13 +151,16 @@ export default { }, methods: { getInputClassesFromProp(classes) { - if (this.inputClass) { - if (typeof this.inputClass === 'function') { - classes = this.inputClass(classes); - } else { - classes.push(this.inputClass); - } + if (!this.inputClass) { + return classes; } + + if (typeof this.inputClass === 'function') { + classes = this.inputClass(classes); + } else { + classes.push(this.inputClass); + } + return classes; }, focus() { diff --git a/src/components/Controls/ExchangeRate.vue b/src/components/Controls/ExchangeRate.vue new file mode 100644 index 00000000..73d3de66 --- /dev/null +++ b/src/components/Controls/ExchangeRate.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/components/SalesInvoice/Templates/BaseTemplate.vue b/src/components/SalesInvoice/Templates/BaseTemplate.vue index 4c32040a..c8bdaf5d 100644 --- a/src/components/SalesInvoice/Templates/BaseTemplate.vue +++ b/src/components/SalesInvoice/Templates/BaseTemplate.vue @@ -1,15 +1,8 @@ diff --git a/src/components/SalesInvoice/Templates/Basic.vue b/src/components/SalesInvoice/Templates/Basic.vue index 0d022a86..6ead0b28 100644 --- a/src/components/SalesInvoice/Templates/Basic.vue +++ b/src/components/SalesInvoice/Templates/Basic.vue @@ -1,36 +1,31 @@ diff --git a/src/components/SalesInvoice/Templates/Business.vue b/src/components/SalesInvoice/Templates/Business.vue index 69e3e5f7..e4c48b49 100644 --- a/src/components/SalesInvoice/Templates/Business.vue +++ b/src/components/SalesInvoice/Templates/Business.vue @@ -1,65 +1,58 @@ - diff --git a/src/components/SalesInvoice/Templates/Minimal.vue b/src/components/SalesInvoice/Templates/Minimal.vue index 158767b5..cd54f0c1 100644 --- a/src/components/SalesInvoice/Templates/Minimal.vue +++ b/src/components/SalesInvoice/Templates/Minimal.vue @@ -1,65 +1,60 @@ - diff --git a/src/pages/InvoiceForm.vue b/src/pages/InvoiceForm.vue index 6347b404..1c9e13e5 100644 --- a/src/pages/InvoiceForm.vue +++ b/src/pages/InvoiceForm.vue @@ -3,6 +3,16 @@ @@ -67,6 +73,7 @@