2
0
mirror of https://github.com/frappe/books.git synced 2025-01-22 22:58:28 +00:00

fix: from account not found

- add a bunch of Payment validations
- add inline form error message display
This commit is contained in:
18alantom 2022-10-08 00:11:50 +05:30
parent ef4e934bfd
commit 7c03b98d67
10 changed files with 265 additions and 73 deletions

View File

@ -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);

View File

@ -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<DocValue | Doc[]> {
parentFieldname?: string;
parentSchemaName?: string;
_links?: Record<string, Doc>;
links?: Record<string, Doc>;
_dirty: boolean = true;
_notInserted: boolean = true;
@ -512,41 +513,67 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
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<Doc | null> {
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<DocValue | Doc[]> {
}
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;

View File

@ -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() {
/**

View File

@ -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<AccountTypeEnum, string[] | undefined>;
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<AccountTypeMap> {
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' };
},

View File

@ -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
);
},
};
}

View File

@ -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,

View File

@ -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.",

View File

@ -135,7 +135,7 @@ export default {
doc.once('afterSync', () => {
this.$emit('new-doc', doc);
this.$router.back();
this.results = []
this.results = [];
});
},
async getCreateFilters() {

View File

@ -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;

View File

@ -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();