diff --git a/backend/database/core.ts b/backend/database/core.ts index 3590d16e..7af66a76 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -15,7 +15,11 @@ import { SchemaMap, TargetField, } from '../../schemas/types'; -import { getRandomString, getValueMapFromList } from '../../utils'; +import { + getIsNullOrUndef, + getRandomString, + getValueMapFromList, +} from '../../utils'; import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types'; import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers'; import { @@ -889,12 +893,13 @@ export default class DatabaseCore extends DatabaseBase { const tableFieldValue = fieldValueMap[field.fieldname] as | FieldValueMap[] - | undefined; - if (tableFieldValue === undefined) { + | undefined + | null; + if (getIsNullOrUndef(tableFieldValue)) { continue; } - for (const child of tableFieldValue) { + for (const child of tableFieldValue!) { this.#prepareChild(schemaName, parentName, child, field, added.length); if ( diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index ee610310..d32e6e14 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -127,6 +127,10 @@ export class Doc extends Observable { } else { this[fieldname] = value ?? this[fieldname] ?? null; } + + if (field.fieldtype === FieldTypeEnum.Table && !this[fieldname]) { + this[fieldname] = []; + } } } @@ -269,6 +273,10 @@ export class Doc extends Observable { } _getChildDoc(docValueMap: Doc | DocValueMap, fieldname: string): Doc { + if (!this.name) { + this.name = getRandomString(); + } + docValueMap.name ??= getRandomString(); // Child Meta Fields @@ -304,6 +312,10 @@ export class Doc extends Observable { for (const field of tableFields) { const childDocs = this.get(field.fieldname) as Doc[]; + if (!childDocs) { + continue; + } + checkForMandatory.push(...childDocs); } @@ -351,13 +363,18 @@ export class Doc extends Observable { await validator(value); } - getValidDict(): DocValueMap { + getValidDict(filterMeta: boolean = false): DocValueMap { + let fields = this.schema.fields; + if (filterMeta) { + fields = this.schema.fields.filter((f) => !f.meta); + } + const data: DocValueMap = {}; - for (const field of this.schema.fields) { + for (const field of fields) { let value = this[field.fieldname] as DocValue | DocValueMap[]; if (Array.isArray(value)) { - value = value.map((doc) => (doc as Doc).getValidDict()); + value = value.map((doc) => (doc as Doc).getValidDict(filterMeta)); } if (isPesa(value)) { @@ -704,25 +721,17 @@ export class Doc extends Observable { return await this.sync(); } - async duplicate(shouldSync: boolean = true): Promise { - const updateMap: DocValueMap = {}; - const docValueMap = this.getValidDict(); - const fieldnames = this.schema.fields.map((f) => f.fieldname); - - for (const fn of fieldnames) { - const value = docValueMap[fn]; - if (getIsNullOrUndef(value)) { + duplicate(): Doc { + const updateMap = this.getValidDict(true); + for (const field in updateMap) { + const value = updateMap[field]; + if (!Array.isArray(value)) { continue; } - if (Array.isArray(value)) { - value.forEach((row) => { - delete row.name; - delete row.parent; - }); + for (const row of value) { + delete row.name; } - - updateMap[fn] = value; } if (this.numberSeries) { @@ -731,12 +740,7 @@ export class Doc extends Observable { updateMap.name = updateMap.name + ' CPY'; } - const doc = this.fyo.doc.getNewDoc(this.schemaName, updateMap, false); - if (shouldSync) { - await doc.sync(); - } - - return doc; + return this.fyo.doc.getNewDoc(this.schemaName, updateMap); } /** diff --git a/models/Transactional/LedgerPosting.ts b/models/Transactional/LedgerPosting.ts index 008bcf3e..dfd34d00 100644 --- a/models/Transactional/LedgerPosting.ts +++ b/models/Transactional/LedgerPosting.ts @@ -26,7 +26,8 @@ export class LedgerPosting { fyo: Fyo; refDoc: Transactional; entries: AccountingLedgerEntry[]; - entryMap: Record; + creditMap: Record; + debitMap: Record; reverted: boolean; accountBalanceChanges: AccountBalanceChange[]; @@ -34,19 +35,20 @@ export class LedgerPosting { this.fyo = fyo; this.refDoc = refDoc; this.entries = []; - this.entryMap = {}; + this.creditMap = {}; + this.debitMap = {}; this.reverted = false; this.accountBalanceChanges = []; } async debit(account: string, amount: Money) { - const ledgerEntry = this._getLedgerEntry(account); + const ledgerEntry = this._getLedgerEntry(account, 'debit'); await ledgerEntry.set('debit', ledgerEntry.debit!.add(amount)); await this._updateAccountBalanceChange(account, 'debit', amount); } async credit(account: string, amount: Money) { - const ledgerEntry = this._getLedgerEntry(account); + const ledgerEntry = this._getLedgerEntry(account, 'credit'); await ledgerEntry.set('credit', ledgerEntry.credit!.add(amount)); await this._updateAccountBalanceChange(account, 'credit', amount); } @@ -101,9 +103,17 @@ export class LedgerPosting { }); } - _getLedgerEntry(account: string): AccountingLedgerEntry { - if (this.entryMap[account]) { - return this.entryMap[account]; + _getLedgerEntry( + account: string, + type: TransactionType + ): AccountingLedgerEntry { + let map = this.creditMap; + if (type === 'debit') { + map = this.debitMap; + } + + if (map[account]) { + return map[account]; } const ledgerEntry = this.fyo.doc.getNewDoc( @@ -122,9 +132,9 @@ export class LedgerPosting { ) as AccountingLedgerEntry; this.entries.push(ledgerEntry); - this.entryMap[account] = ledgerEntry; + map[account] = ledgerEntry; - return this.entryMap[account]; + return map[account]; } _validateIsEqual() { diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts index 7702d255..7a9e16d1 100644 --- a/models/baseModels/JournalEntry/JournalEntry.ts +++ b/models/baseModels/JournalEntry/JournalEntry.ts @@ -13,12 +13,12 @@ import Money from 'pesa/dist/types/src/money'; import { LedgerPosting } from '../../Transactional/LedgerPosting'; export class JournalEntry extends Transactional { - accounts: Doc[] = []; + accounts?: Doc[]; async getPosting() { const posting: LedgerPosting = new LedgerPosting(this, this.fyo); - for (const row of this.accounts) { + for (const row of this.accounts ?? []) { const debit = row.debit as Money; const credit = row.credit as Money; const account = row.account as string; diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index 9b60ccb2..932df43d 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -11,9 +11,11 @@ import { validateEmail, validatePhoneNumber, } from 'fyo/model/validationFunction'; +import Money from 'pesa/dist/types/src/money'; import { PartyRole } from './types'; export class Party extends Doc { + outstandingAmount?: Money; async updateOutstandingAmount() { /** * If Role === "Both" then outstanding Amount diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 88345efd..9232f163 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -73,7 +73,7 @@ export class Payment extends Transactional { updateAmountOnReferenceUpdate() { this.amount = this.fyo.pesa(0); - for (const paymentReference of this.for as Doc[]) { + for (const paymentReference of (this.for ?? []) as Doc[]) { this.amount = (this.amount as Money).add( paymentReference.amount as Money ); @@ -163,16 +163,28 @@ export class Payment extends Transactional { } async getPosting() { - const account = this.account as string; - const paymentAccount = this.paymentAccount as string; - const amount = this.amount as Money; - const writeoff = this.writeoff as Money; + /** + * account : From Account + * paymentAccount : To Account + * + * if Receive + * - account : Debtors, etc + * - paymentAccount : Cash, Bank, etc + * + * if Pay + * - account : Cash, Bank, etc + * - paymentAccount : Creditors, etc + */ const posting: LedgerPosting = new LedgerPosting(this, this.fyo); - await posting.debit(paymentAccount as string, amount.sub(writeoff)); - await posting.credit(account as string, amount.sub(writeoff)); + const paymentAccount = this.paymentAccount as string; + const account = this.account as string; + const amount = this.amount as Money; - this.applyWriteOffPosting(posting); + await posting.debit(paymentAccount as string, amount); + await posting.credit(account as string, amount); + + await this.applyWriteOffPosting(posting); return posting; } @@ -183,11 +195,12 @@ export class Payment extends Transactional { } const account = this.account as string; + const paymentAccount = this.paymentAccount as string; const writeOffAccount = this.fyo.singles.AccountingSettings! .writeOffAccount as string; if (this.paymentType === 'Pay') { - await posting.credit(account, writeoff); + await posting.credit(paymentAccount, writeoff); await posting.debit(writeOffAccount, writeoff); } else { await posting.debit(account, writeoff); @@ -277,7 +290,7 @@ export class Payment extends Transactional { } async _revertReferenceOutstanding() { - for (const ref of this.for as PaymentFor[]) { + for (const ref of (this.for ?? []) as PaymentFor[]) { const refDoc = await this.fyo.doc.getDoc( ref.referenceType!, ref.referenceName! @@ -318,6 +331,32 @@ export class Payment extends Transactional { }, dependsOn: ['paymentMethod', 'paymentType'], }, + paymentType: { + formula: async () => { + if (!this.party) { + return; + } + const partyDoc = await this.fyo.doc.getDoc( + ModelNameEnum.Party, + this.party + ); + if (partyDoc.role === 'Supplier') { + return 'Pay'; + } else if (partyDoc.role === 'Customer') { + return 'Receive'; + } + + const outstanding = partyDoc.outstandingAmount as Money; + if (outstanding?.isZero() ?? true) { + return ''; + } + + if (outstanding?.isPositive()) { + return 'Receive'; + } + return 'Pay'; + }, + }, amount: { formula: async () => this.getSum('for', 'amount', false), dependsOn: ['for'], @@ -332,7 +371,10 @@ export class Payment extends Transactional { ); } - if ((this.for as Doc[]).length === 0) return; + if (((this.for ?? []) as Doc[]).length === 0) { + return; + } + const amount = this.getSum('for', 'amount', false); if ((value as Money).gt(amount)) { diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 7048d310..e57780e2 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -113,7 +113,7 @@ }, { "fieldname": "writeoff", - "label": "Write Off / Refund", + "label": "Write Off", "fieldtype": "Currency" }, { diff --git a/src/components/Controls/Table.vue b/src/components/Controls/Table.vue index a4b9b6b7..0ecf3f39 100644 --- a/src/components/Controls/Table.vue +++ b/src/components/Controls/Table.vue @@ -21,7 +21,11 @@ -
+
{{ t`${value.length} rows` }}
diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 52ade811..a10ee395 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -7,6 +7,7 @@ import { t } from 'fyo'; import { Doc } from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { getActions } from 'fyo/utils'; +import { ModelNameEnum } from 'models/types'; import { handleErrorWithDialog } from 'src/errorHandling'; import { fyo } from 'src/initFyo'; import router from 'src/router'; @@ -288,15 +289,28 @@ function getDeleteAction(doc: Doc): Action { }; } +async function openEdit(doc: Doc) { + const isFormEdit = [ + ModelNameEnum.SalesInvoice, + ModelNameEnum.PurchaseInvoice, + ModelNameEnum.JournalEntry, + ].includes(doc.schemaName as ModelNameEnum); + + if (isFormEdit) { + return await routeTo(`/edit/${doc.schemaName}/${doc.name!}`); + } + + await openQuickEdit({ schemaName: doc.schemaName, name: doc.name! }); +} + function getDuplicateAction(doc: Doc): Action { const isSubmittable = !!doc.schema.isSubmittable; return { label: t`Duplicate`, condition: (doc: Doc) => !!( - ((isSubmittable && doc && doc.submitted) || !isSubmittable) && - !doc.notInserted && - !(doc.cancelled || false) + ((isSubmittable && doc.submitted) || !isSubmittable) && + !doc.notInserted ), async action() { await showMessageDialog({ @@ -306,7 +320,8 @@ function getDuplicateAction(doc: Doc): Action { label: t`Yes`, async action() { try { - doc.duplicate(); + const dupe = await doc.duplicate(); + await openEdit(dupe); return true; } catch (err) { handleErrorWithDialog(err as Error, doc);