diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index e67ef174..88b63cf6 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -70,7 +70,7 @@ export class DocHandler { return doc; } - doc = this.getNewDoc(schemaName, { name }); + doc = this.getNewDoc(schemaName, { name }, false); await doc.load(); this.#addToCache(doc); diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 66797c92..e4389f4e 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -7,6 +7,7 @@ import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors'; import Observable from 'fyo/utils/observable'; import { Money } from 'pesa'; import { + DynamicLinkField, Field, FieldTypeEnum, OptionField, @@ -60,7 +61,7 @@ export class Doc extends Observable { parentFieldname?: string; parentSchemaName?: string; - _links?: Record; + links?: Record; _dirty: boolean = true; _notInserted: boolean = true; @@ -512,41 +513,67 @@ export class Doc extends Observable { } async loadLinks() { - this._links = {}; + this.links ??= {}; const linkFields = this.schema.fields.filter( - (f) => f.fieldtype === FieldTypeEnum.Link || f.inline + ({ fieldtype }) => + fieldtype === FieldTypeEnum.Link || + fieldtype === FieldTypeEnum.DynamicLink ); - for (const f of linkFields) { - await this.loadLink(f.fieldname); + for (const field of linkFields) { + await this._loadLink(field); } } - async loadLink(fieldname: string) { - this._links ??= {}; - const field = this.fieldMap[fieldname] as TargetField; - if (field === undefined) { + async _loadLink(field: Field) { + if (field.fieldtype === FieldTypeEnum.Link) { + return await this._loadLinkField(field as TargetField); + } + + if (field.fieldtype === FieldTypeEnum.DynamicLink) { + return await this._loadDynamicLinkField(field as DynamicLinkField); + } + } + + async _loadLinkField(field: TargetField) { + const { fieldname, target } = field; + const value = this.get(fieldname) as string | undefined; + if (!value || !target) { return; } - const value = this.get(fieldname); - if (getIsNullOrUndef(value) || field.target === undefined) { + await this._loadLinkDoc(fieldname, target, value); + } + + async _loadDynamicLinkField(field: DynamicLinkField) { + const { fieldname, references } = field; + const value = this.get(fieldname) as string | undefined; + const reference = this.get(references) as string | undefined; + if (!value || !reference) { return; } - this._links[fieldname] = await this.fyo.doc.getDoc( - field.target, - value as string - ); + await this._loadLinkDoc(fieldname, reference, value); + } + + async _loadLinkDoc(fieldname: string, schemaName: string, name: string) { + this.links![fieldname] = await this.fyo.doc.getDoc(schemaName, name); } getLink(fieldname: string): Doc | null { - const link = this._links?.[fieldname]; - if (link === undefined) { + return this.links?.[fieldname] ?? null; + } + + async loadAndGetLink(fieldname: string): Promise { + if (!this?.[fieldname]) { return null; } - return link; + if (this.links?.[fieldname]?.name !== this[fieldname]) { + await this.loadLinks(); + } + + return this.links?.[fieldname] ?? null; } async _syncValues(data: DocValueMap) { @@ -672,8 +699,8 @@ export class Doc extends Observable { } async _applyFormulaForFields(doc: Doc, fieldname?: string) { - const formulaFields = Object.keys(this.formulas).map( - (fn) => this.fieldMap[fn] + const formulaFields = this.schema.fields.filter( + ({ fieldname }) => this.formulas?.[fieldname] ); let changed = false; diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index db500e4f..bb8c26cd 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -15,6 +15,8 @@ import { Money } from 'pesa'; import { PartyRole } from './types'; export class Party extends Doc { + role?: PartyRole; + defaultAccount?: string; outstandingAmount?: Money; async updateOutstandingAmount() { /** diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 7abffeb3..67670793 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -23,16 +23,22 @@ import { LedgerPosting } from 'models/Transactional/LedgerPosting'; import { Transactional } from 'models/Transactional/Transactional'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; +import { QueryFilter } from 'utils/db/types'; +import { AccountTypeEnum } from '../Account/types'; import { Invoice } from '../Invoice/Invoice'; import { Party } from '../Party/Party'; import { PaymentFor } from '../PaymentFor/PaymentFor'; import { PaymentMethod, PaymentType } from './types'; +type AccountTypeMap = Record; + export class Payment extends Transactional { party?: string; amount?: Money; writeoff?: Money; paymentType?: PaymentType; + for?: PaymentFor[]; + _accountsMap?: AccountTypeMap; async change({ changed }: ChangeArg) { if (changed === 'for') { @@ -100,12 +106,44 @@ export class Payment extends Transactional { return; } + await this.validateFor(); this.validateAccounts(); this.validateTotalReferenceAmount(); this.validateWriteOffAccount(); await this.validateReferences(); } + async validateFor() { + for (const childDoc of this.for ?? []) { + const referenceName = childDoc.referenceName; + const referenceType = childDoc.referenceType; + + const refDoc = (await this.fyo.doc.getDoc( + childDoc.referenceType!, + childDoc.referenceName + )) as Invoice; + + if (referenceName && referenceType && !refDoc) { + throw new ValidationError( + t`${referenceType} of type ${this.fyo.schemaMap?.[referenceType] + ?.label!} does not exist` + ); + } + console.log(refDoc); + + if (!refDoc) { + continue; + } + + if (refDoc?.party !== this.party) { + throw new ValidationError( + t`${refDoc.name!} party ${refDoc.party!} is different from ${this + .party!}` + ); + } + } + } + validateAccounts() { if (this.paymentAccount !== this.account || !this.account) { return; @@ -319,46 +357,118 @@ export class Payment extends Transactional { static defaults: DefaultMap = { date: () => new Date().toISOString() }; + async _getAccountsMap(): Promise { + if (this._accountsMap) { + return this._accountsMap; + } + + const accounts = (await this.fyo.db.getAll(ModelNameEnum.Account, { + fields: ['name', 'accountType'], + filters: { + accountType: [ + 'in', + [ + AccountTypeEnum.Bank, + AccountTypeEnum.Cash, + AccountTypeEnum.Payable, + AccountTypeEnum.Receivable, + ], + ], + }, + })) as { name: string; accountType: AccountTypeEnum }[]; + + return (this._accountsMap = accounts.reduce((acc, ac) => { + acc[ac.accountType] ??= []; + acc[ac.accountType]!.push(ac.name); + return acc; + }, {} as AccountTypeMap)); + } + + async _getReferenceAccount() { + const account = await this._getAccountFromParty(); + if (!account) { + return await this._getAccountFromFor(); + } + + return account; + } + + async _getAccountFromParty() { + const party = (await this.loadAndGetLink('party')) as Party | null; + if (!party || party.role === 'Both') { + return null; + } + + return party.defaultAccount ?? null; + } + + async _getAccountFromFor() { + const reference = this?.for?.[0]; + if (!reference) { + return null; + } + + const refDoc = (await reference.loadAndGetLink( + 'referenceName' + )) as Invoice | null; + + return (refDoc?.account ?? null) as string | null; + } + formulas: FormulaMap = { account: { formula: async () => { - const hasCash = await this.fyo.db.exists(ModelNameEnum.Account, 'Cash'); - if ( - this.paymentMethod === 'Cash' && - this.paymentType === 'Pay' && - hasCash - ) { - return 'Cash'; + const accountsMap = await this._getAccountsMap(); + if (this.paymentType === 'Receive') { + return ( + (await this._getReferenceAccount()) ?? + accountsMap[AccountTypeEnum.Receivable]?.[0] ?? + null + ); + } + + if (this.paymentMethod === 'Cash') { + return accountsMap[AccountTypeEnum.Cash]?.[0] ?? null; + } + + if (this.paymentMethod !== 'Cash') { + return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null; } return null; }, - dependsOn: ['paymentMethod', 'paymentType'], + dependsOn: ['paymentMethod', 'paymentType', 'party'], }, paymentAccount: { formula: async () => { - const hasCash = await this.fyo.db.exists(ModelNameEnum.Account, 'Cash'); - if ( - this.paymentMethod === 'Cash' && - this.paymentType === 'Receive' && - hasCash - ) { - return 'Cash'; + const accountsMap = await this._getAccountsMap(); + if (this.paymentType === 'Pay') { + return ( + (await this._getReferenceAccount()) ?? + accountsMap[AccountTypeEnum.Payable]?.[0] ?? + null + ); + } + + if (this.paymentMethod === 'Cash') { + return accountsMap[AccountTypeEnum.Cash]?.[0] ?? null; + } + + if (this.paymentMethod !== 'Cash') { + return accountsMap[AccountTypeEnum.Bank]?.[0] ?? null; } return null; }, - dependsOn: ['paymentMethod', 'paymentType'], + dependsOn: ['paymentMethod', 'paymentType', 'party'], }, paymentType: { formula: async () => { if (!this.party) { return; } - const partyDoc = await this.fyo.doc.getDoc( - ModelNameEnum.Party, - this.party - ); + + const partyDoc = (await this.loadAndGetLink('party')) as Party; if (partyDoc.role === 'Supplier') { return 'Pay'; } else if (partyDoc.role === 'Customer') { @@ -426,6 +536,18 @@ export class Payment extends Transactional { }; static filters: FiltersMap = { + party: (doc: Doc) => { + const paymentType = (doc as Payment).paymentType; + if (paymentType === 'Pay') { + return { role: ['in', ['Supplier', 'Both']] } as QueryFilter; + } + + if (paymentType === 'Receive') { + return { role: ['in', ['Customer', 'Both']] } as QueryFilter; + } + + return {}; + }, numberSeries: () => { return { referenceType: 'Payment' }; }, diff --git a/models/baseModels/PaymentFor/PaymentFor.ts b/models/baseModels/PaymentFor/PaymentFor.ts index 52aec1e6..4dc40ed0 100644 --- a/models/baseModels/PaymentFor/PaymentFor.ts +++ b/models/baseModels/PaymentFor/PaymentFor.ts @@ -1,5 +1,8 @@ +import { t } from 'fyo'; +import { DocValue } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; -import { FiltersMap, FormulaMap } from 'fyo/model/types'; +import { FiltersMap, FormulaMap, ValidationMap } from 'fyo/model/types'; +import { NotFoundError } from 'fyo/utils/errors'; import { ModelNameEnum } from 'models/types'; import { Money } from 'pesa'; import { PartyRoleEnum } from '../Party/types'; @@ -18,24 +21,37 @@ export class PaymentFor extends Doc { return; } - const party = this.parentdoc!.party; - if (party === undefined) { + const party = await this.parentdoc?.loadAndGetLink('party'); + if (!party) { return ModelNameEnum.SalesInvoice; } - const role = await this.fyo.getValue( - ModelNameEnum.Party, - party, - 'role' - ); - - if (role === PartyRoleEnum.Supplier) { + if (party.role === PartyRoleEnum.Supplier) { return ModelNameEnum.PurchaseInvoice; } return ModelNameEnum.SalesInvoice; }, }, + referenceName: { + formula: async () => { + if (!this.referenceName || !this.referenceType) { + return this.referenceName; + } + + const exists = await this.fyo.db.exists( + this.referenceType, + this.referenceName + ); + + if (!exists) { + return null; + } + + return this.referenceName; + }, + dependsOn: ['referenceType'], + }, amount: { formula: async () => { if (!this.referenceName) { @@ -74,4 +90,24 @@ export class PaymentFor extends Doc { return { ...baseFilters, party }; }, }; + + validations: ValidationMap = { + referenceName: async (value: DocValue) => { + console.log(value); + const exists = await this.fyo.db.exists( + this.referenceType!, + value as string + ); + if (exists) { + return; + } + + throw new NotFoundError( + t`${this.fyo.schemaMap[this.referenceType!]?.label!} ${ + value as string + } does not exist`, + false + ); + }, + }; } diff --git a/models/helpers.ts b/models/helpers.ts index e78580ef..9e82d41d 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -34,7 +34,7 @@ export function getInvoiceActions( await openQuickEdit({ schemaName: 'Payment', name: payment.name as string, - hideFields: ['party', 'date', hideAccountField, 'paymentType', 'for'], + hideFields: ['party', 'date', 'paymentType', 'for'], defaults: { party, [hideAccountField]: doc.account, diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 3482e028..f2deae3a 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -27,14 +27,6 @@ "fieldtype": "Date", "required": true }, - { - "fieldname": "account", - "label": "From Account", - "fieldtype": "Link", - "target": "Account", - "create": true, - "required": true - }, { "fieldname": "paymentType", "label": "Payment Type", @@ -52,15 +44,6 @@ ], "required": true }, - { - "fieldname": "paymentAccount", - "label": "To Account", - "placeholder": "To Account", - "fieldtype": "Link", - "target": "Account", - "create": true, - "required": true - }, { "fieldname": "numberSeries", "label": "Number Series", @@ -92,6 +75,23 @@ "default": "Cash", "required": true }, + { + "fieldname": "account", + "label": "From Account", + "fieldtype": "Link", + "target": "Account", + "create": true, + "required": true + }, + { + "fieldname": "paymentAccount", + "label": "To Account", + "placeholder": "To Account", + "fieldtype": "Link", + "target": "Account", + "create": true, + "required": true + }, { "fieldname": "referenceId", "label": "Ref. / Cheque No.", diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index 4c684f33..0f632ff8 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -135,7 +135,7 @@ export default { doc.once('afterSync', () => { this.$emit('new-doc', doc); this.$router.back(); - this.results = [] + this.results = []; }); }, async getCreateFilters() { diff --git a/src/components/SalesInvoice/Templates/BaseTemplate.vue b/src/components/SalesInvoice/Templates/BaseTemplate.vue index c8bdaf5d..6bbef962 100644 --- a/src/components/SalesInvoice/Templates/BaseTemplate.vue +++ b/src/components/SalesInvoice/Templates/BaseTemplate.vue @@ -4,10 +4,10 @@ export default { props: { doc: Object, printSettings: Object }, data: () => ({ party: null, companyAddress: null, partyAddress: null }), async mounted() { - await this.printSettings.loadLink('address'); + await this.printSettings.loadLinks(); this.companyAddress = this.printSettings.getLink('address'); - await this.doc.loadLink('party'); + await this.doc.loadLinks(); this.party = this.doc.getLink('party'); this.partyAddress = this.party.getLink('address')?.addressDisplay ?? null; diff --git a/src/components/TwoColumnForm.vue b/src/components/TwoColumnForm.vue index a5434dc6..251b0c3c 100644 --- a/src/components/TwoColumnForm.vue +++ b/src/components/TwoColumnForm.vue @@ -265,7 +265,12 @@ export default { return; } - await this.inlineEditDoc.sync(); + try { + await this.inlineEditDoc.sync(); + } catch (error) { + return await handleErrorWithDialog(error, this.inlineEditDoc) + } + await this.onChangeCommon(df, this.inlineEditDoc.name); await this.doc.loadLinks();