2
0
mirror of https://github.com/frappe/books.git synced 2024-12-23 03:19:01 +00:00

incr: abstract away transactional

- add deletion
- fix reversion
This commit is contained in:
18alantom 2022-05-03 18:43:47 +05:30
parent 9a012207f1
commit 20ea214a4b
26 changed files with 738 additions and 531 deletions

View File

@ -234,6 +234,10 @@ export default class DatabaseCore extends DatabaseBase {
options: GetAllOptions = {} options: GetAllOptions = {}
): Promise<FieldValueMap[]> { ): Promise<FieldValueMap[]> {
const schema = this.schemaMap[schemaName] as Schema; const schema = this.schemaMap[schemaName] as Schema;
if (schema === undefined) {
throw new NotFoundError(`schema ${schemaName} not found`);
}
const hasCreated = !!schema.fields.find((f) => f.fieldname === 'created'); const hasCreated = !!schema.fields.find((f) => f.fieldname === 'created');
const { const {
fields = ['name', ...(schema.keywordFields ?? [])], fields = ['name', ...(schema.keywordFields ?? [])],

View File

@ -25,6 +25,7 @@ import {
import { setName } from './naming'; import { setName } from './naming';
import { import {
Action, Action,
ChangeArg,
CurrenciesMap, CurrenciesMap,
DefaultMap, DefaultMap,
EmptyMessageMap, EmptyMessageMap,
@ -37,7 +38,7 @@ import {
ReadOnlyMap, ReadOnlyMap,
RequiredMap, RequiredMap,
TreeViewSettings, TreeViewSettings,
ValidationMap ValidationMap,
} from './types'; } from './types';
import { validateOptions, validateRequired } from './validationFunction'; import { validateOptions, validateRequired } from './validationFunction';
@ -106,6 +107,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
return fieldnames.map((f) => this.fieldMap[f]); return fieldnames.map((f) => this.fieldMap[f]);
} }
get isSubmitted() {
return !!this.submitted && !this.cancelled;
}
get isCancelled() {
return !!this.submitted && !!this.cancelled;
}
_setValuesWithoutChecks(data: DocValueMap) { _setValuesWithoutChecks(data: DocValueMap) {
for (const field of this.schema.fields) { for (const field of this.schema.fields) {
const fieldname = field.fieldname; const fieldname = field.fieldname;
@ -131,7 +140,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
// set value and trigger change // set value and trigger change
async set( async set(
fieldname: string | DocValueMap, fieldname: string | DocValueMap,
value?: DocValue | Doc[] value?: DocValue | Doc[] | DocValueMap[]
): Promise<boolean> { ): Promise<boolean> {
if (typeof fieldname === 'object') { if (typeof fieldname === 'object') {
return await this.setMultiple(fieldname as DocValueMap); return await this.setMultiple(fieldname as DocValueMap);
@ -143,10 +152,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
this._setDirty(true); this._setDirty(true);
if (Array.isArray(value)) { if (Array.isArray(value)) {
this[fieldname] = value.map((row, i) => { for (const row of value) {
row.idx = i; this.push(fieldname, row);
return row; }
});
} else { } else {
const field = this.fieldMap[fieldname]; const field = this.fieldMap[fieldname];
await this._validateField(field, value); await this._validateField(field, value);
@ -177,7 +185,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
return hasSet; return hasSet;
} }
_canSet(fieldname: string, value?: DocValue | Doc[]): boolean { _canSet(
fieldname: string,
value?: DocValue | Doc[] | DocValueMap[]
): boolean {
if (fieldname === 'numberSeries' && !this.notInserted) { if (fieldname === 'numberSeries' && !this.notInserted) {
return false; return false;
} }
@ -610,6 +621,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
} }
async delete() { async delete() {
if (this.schema.isSubmittable && !this.isCancelled) {
return;
}
await this.trigger('beforeDelete'); await this.trigger('beforeDelete');
await this.fyo.db.delete(this.schemaName, this.name!); await this.fyo.db.delete(this.schemaName, this.name!);
await this.trigger('afterDelete'); await this.trigger('afterDelete');
@ -619,7 +634,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
} }
async submit() { async submit() {
if (!this.schema.isSubmittable || this.cancelled) { if (!this.schema.isSubmittable || this.submitted || this.cancelled) {
return; return;
} }
@ -631,7 +646,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
} }
async cancel() { async cancel() {
if (!this.schema.isSubmittable || !this.submitted) { if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
return; return;
} }
@ -727,14 +742,24 @@ export class Doc extends Observable<DocValue | Doc[]> {
return doc; return doc;
} }
/**
* Lifecycle Methods
*
* Abstractish methods that are called using `this.trigger`.
* These are to be overridden if required when subclassing.
*/
async change(ch: ChangeArg) {}
async validate() {}
async beforeSync() {} async beforeSync() {}
async afterSync() {} async afterSync() {}
async beforeDelete() {}
async afterDelete() {}
async beforeSubmit() {} async beforeSubmit() {}
async afterSubmit() {} async afterSubmit() {}
async beforeRename() {}
async afterRename() {}
async beforeCancel() {} async beforeCancel() {}
async afterCancel() {} async afterCancel() {}
async beforeDelete() {}
async afterDelete() {}
formulas: FormulaMap = {}; formulas: FormulaMap = {};
validations: ValidationMap = {}; validations: ValidationMap = {};

View File

@ -35,6 +35,8 @@ export type CurrenciesMap = Record<string, GetCurrency | undefined>;
export type HiddenMap = Record<string, Hidden | undefined>; export type HiddenMap = Record<string, Hidden | undefined>;
export type ReadOnlyMap = Record<string, ReadOnly | undefined>; export type ReadOnlyMap = Record<string, ReadOnly | undefined>;
export type ChangeArg = { doc: Doc; changed: string };
/** /**
* Should add this for hidden too * Should add this for hidden too
*/ */

View File

@ -0,0 +1,207 @@
import { Fyo, t } from 'fyo';
import { ValidationError } from 'fyo/utils/errors';
import { Account } from 'models/baseModels/Account/Account';
import { AccountingLedgerEntry } from 'models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
import { ModelNameEnum } from 'models/types';
import Money from 'pesa/dist/types/src/money';
import { Transactional } from './Transactional';
import { AccountBalanceChange, TransactionType } from './types';
/**
* # LedgerPosting
*
* Class that maintains a set of AccountingLedgerEntries pertaining to
* a single Transactional doc, example a Payment entry.
*
* For each account touched in a transactional doc a separate ledger entry is
* created.
*
* This is done using the `debit(...)` and `credit(...)` methods which also
* keep track of the change in balance to an account.
*
* Unless `post()` or `postReverese()` is called the entries aren't made.
*/
export class LedgerPosting {
fyo: Fyo;
refDoc: Transactional;
entries: AccountingLedgerEntry[];
entryMap: Record<string, AccountingLedgerEntry>;
reverted: boolean;
accountBalanceChanges: AccountBalanceChange[];
constructor(refDoc: Transactional, fyo: Fyo) {
this.fyo = fyo;
this.refDoc = refDoc;
this.entries = [];
this.entryMap = {};
this.reverted = false;
this.accountBalanceChanges = [];
}
async debit(account: string, amount: Money) {
const ledgerEntry = this._getLedgerEntry(account);
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);
await ledgerEntry.set('credit', ledgerEntry.credit!.add(amount));
await this._updateAccountBalanceChange(account, 'credit', amount);
}
async post() {
this._validateIsEqual();
await this._sync();
}
async postReverse() {
this._validateIsEqual();
await this._syncReverse();
}
validate() {
this._validateIsEqual();
}
async makeRoundOffEntry() {
const { debit, credit } = this._getTotalDebitAndCredit();
const difference = debit.sub(credit);
const absoluteValue = difference.abs();
if (absoluteValue.eq(0)) {
return;
}
const roundOffAccount = await this._getRoundOffAccount();
if (difference.gt(0)) {
this.credit(roundOffAccount, absoluteValue);
} else {
this.debit(roundOffAccount, absoluteValue);
}
}
async _updateAccountBalanceChange(
name: string,
type: TransactionType,
amount: Money
) {
const accountDoc = (await this.fyo.doc.getDoc('Account', name)) as Account;
let change: Money;
if (accountDoc.isDebit) {
change = type === 'debit' ? amount : amount.neg();
} else {
change = type === 'credit' ? amount : amount.neg();
}
this.accountBalanceChanges.push({
name,
change,
});
}
_getLedgerEntry(account: string): AccountingLedgerEntry {
if (this.entryMap[account]) {
return this.entryMap[account];
}
const ledgerEntry = this.fyo.doc.getNewDoc(
ModelNameEnum.AccountingLedgerEntry,
{
account: account,
party: (this.refDoc.party as string) ?? '',
date: this.refDoc.date as string | Date,
referenceType: this.refDoc.schemaName,
referenceName: this.refDoc.name!,
reverted: this.reverted,
debit: this.fyo.pesa(0),
credit: this.fyo.pesa(0),
}
) as AccountingLedgerEntry;
this.entries.push(ledgerEntry);
this.entryMap[account] = ledgerEntry;
return this.entryMap[account];
}
_validateIsEqual() {
const { debit, credit } = this._getTotalDebitAndCredit();
if (debit.eq(credit)) {
return;
}
throw new ValidationError(
t`Total Debit: ${this.fyo.format(
debit,
'Currency'
)} must be equal to Total Credit: ${this.fyo.format(credit, 'Currency')}`
);
}
_getTotalDebitAndCredit() {
let debit = this.fyo.pesa(0);
let credit = this.fyo.pesa(0);
for (const entry of this.entries) {
debit = debit.add(entry.debit!);
credit = credit.add(entry.credit!);
}
return { debit, credit };
}
async _sync() {
await this._syncLedgerEntries();
await this._syncBalanceChanges();
}
async _syncLedgerEntries() {
for (const entry of this.entries) {
await entry.sync();
}
}
async _syncReverse() {
await this._syncReverseLedgerEntries();
for (const entry of this.accountBalanceChanges) {
entry.change = (entry.change as Money).neg();
}
await this._syncBalanceChanges();
}
async _syncBalanceChanges() {
for (const { name, change } of this.accountBalanceChanges) {
const accountDoc = await this.fyo.doc.getDoc(ModelNameEnum.Account, name);
const balance = accountDoc.get('balance') as Money;
await accountDoc.setAndSync('balance', balance.add(change));
}
}
async _syncReverseLedgerEntries() {
const data = (await this.fyo.db.getAll('AccountingLedgerEntry', {
fields: ['name'],
filters: {
referenceType: this.refDoc.schemaName,
referenceName: this.refDoc.name!,
reverted: false,
},
})) as { name: string }[];
for (const { name } of data) {
const doc = (await this.fyo.doc.getDoc(
'AccountingLedgerEntry',
name
)) as AccountingLedgerEntry;
await doc.revert();
}
}
async _getRoundOffAccount() {
return (await this.fyo.getValue(
ModelNameEnum.AccountingSettings,
'roundOffAccount'
)) as string;
}
}

View File

@ -0,0 +1,57 @@
import { Doc } from 'fyo/model/doc';
import { ModelNameEnum } from 'models/types';
import { LedgerPosting } from './LedgerPosting';
/**
* # Transactional
*
* Any model that creates ledger entries on submit should extend the
* `Transactional` model.
*
* Example of transactional models:
* - Invoice
* - Payment
* - JournalEntry
*
* Basically it does the following:
* - `afterSubmit`: create the ledger entries.
* - `afterCancel`: create reverse ledger entries.
* - `afterDelete`: delete the normal and reversed ledger entries.
*/
export abstract class Transactional extends Doc {
isTransactional = true;
abstract getPosting(): Promise<LedgerPosting>;
async validate() {
const posting = await this.getPosting();
posting.validate();
}
async afterSubmit(): Promise<void> {
const posting = await this.getPosting();
await posting.post();
}
async afterCancel(): Promise<void> {
const posting = await this.getPosting();
await posting.postReverse();
}
async afterDelete(): Promise<void> {
const ledgerEntryIds = (await this.fyo.db.getAll(
ModelNameEnum.AccountingLedgerEntry,
{
fields: ['name'],
filters: {
referenceType: this.schemaName,
referenceName: this.name!,
},
}
)) as { name: string }[];
for (const { name } of ledgerEntryIds) {
await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name);
}
}
}

View File

@ -4,24 +4,22 @@ import Money from 'pesa/dist/types/src/money';
export interface LedgerPostingOptions { export interface LedgerPostingOptions {
reference: Doc; reference: Doc;
party?: string; party?: string;
date?: string;
description?: string;
} }
export interface LedgerEntry { export interface LedgerEntry {
account: string; account: string;
party: string; party: string;
date: string; date: string;
referenceType: string; referenceType: string;
referenceName: string; referenceName: string;
description?: string;
reverted: boolean; reverted: boolean;
debit: Money; debit: Money;
credit: Money; credit: Money;
} }
export interface AccountEntry { export interface AccountBalanceChange {
name: string; name: string;
balanceChange: Money; change: Money;
} }
export type TransactionType = 'credit' | 'debit'; export type TransactionType = 'credit' | 'debit';

View File

@ -7,13 +7,25 @@ import {
TreeViewSettings, TreeViewSettings,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { QueryFilter } from 'utils/db/types'; import { QueryFilter } from 'utils/db/types';
import { AccountRootType, AccountType } from './types'; import { AccountRootType, AccountRootTypeEnum, AccountType } from './types';
export class Account extends Doc { export class Account extends Doc {
rootType?: AccountRootType; rootType?: AccountRootType;
accountType?: AccountType; accountType?: AccountType;
parentAccount?: string; parentAccount?: string;
get isDebit() {
const debitAccounts = [
AccountRootTypeEnum.Asset,
AccountRootTypeEnum.Expense,
] as AccountRootType[];
return debitAccounts.includes(this.rootType!);
}
get isCredit() {
return !this.isDebit;
}
static defaults: DefaultMap = { static defaults: DefaultMap = {
/** /**
* NestedSet indices are actually not used * NestedSet indices are actually not used
@ -55,7 +67,7 @@ export class Account extends Doc {
} }
static filters: FiltersMap = { static filters: FiltersMap = {
parentAccount: (doc: Account) => { parentAccount: (doc: Doc) => {
const filter: QueryFilter = { const filter: QueryFilter = {
isGroup: true, isGroup: true,
}; };

View File

@ -1,7 +1,44 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types'; import { ListViewSettings } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import Money from 'pesa/dist/types/src/money';
export class AccountingLedgerEntry extends Doc { export class AccountingLedgerEntry extends Doc {
date?: string | Date;
account?: string;
party?: string;
debit?: Money;
credit?: Money;
referenceType?: string;
referenceName?: string;
balance?: Money;
reverted?: boolean;
async revert() {
if (this.reverted) {
return;
}
await this.set('reverted', true);
const revertedEntry = this.fyo.doc.getNewDoc(
ModelNameEnum.AccountingLedgerEntry,
{
account: this.account,
party: this.party,
date: new Date(),
referenceType: this.referenceType,
referenceName: this.referenceName,
debit: this.credit,
credit: this.debit,
reverted: true,
reverts: this.name,
}
);
await this.sync();
await revertedEntry.sync();
}
static getListViewSettings(): ListViewSettings { static getListViewSettings(): ListViewSettings {
return { return {
columns: ['account', 'party', 'debit', 'credit', 'balance'], columns: ['account', 'party', 'debit', 'credit', 'balance'],

View File

@ -2,7 +2,7 @@ import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types'; import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
import { getExchangeRate } from 'models/helpers'; import { getExchangeRate } from 'models/helpers';
import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { getIsNullOrUndef } from 'utils'; import { getIsNullOrUndef } from 'utils';
@ -11,7 +11,7 @@ import { Payment } from '../Payment/Payment';
import { Tax } from '../Tax/Tax'; import { Tax } from '../Tax/Tax';
import { TaxSummary } from '../TaxSummary/TaxSummary'; import { TaxSummary } from '../TaxSummary/TaxSummary';
export abstract class Invoice extends Doc { export abstract class Invoice extends Transactional {
_taxes: Record<string, Tax> = {}; _taxes: Record<string, Tax> = {};
taxes?: TaxSummary[]; taxes?: TaxSummary[];
@ -26,34 +26,12 @@ export abstract class Invoice extends Doc {
submitted?: boolean; submitted?: boolean;
cancelled?: boolean; cancelled?: boolean;
abstract getPosting(): Promise<LedgerPosting>;
get isSales() { get isSales() {
return this.schemaName === 'SalesInvoice'; return this.schemaName === 'SalesInvoice';
} }
async getPayments() {
const payments = await this.fyo.db.getAll('PaymentFor', {
fields: ['parent'],
filters: { referenceName: this.name! },
orderBy: 'name',
});
if (payments.length != 0) {
return payments;
}
return [];
}
async beforeSync() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async afterSubmit() { async afterSubmit() {
// post ledger entries await super.afterSubmit();
const entries = await this.getPosting();
await entries.post();
// update outstanding amounts // update outstanding amounts
await this.fyo.db.update(this.schemaName, { await this.fyo.db.update(this.schemaName, {
@ -65,29 +43,48 @@ export abstract class Invoice extends Doc {
await party.updateOutstandingAmount(); await party.updateOutstandingAmount();
} }
async afterCancel() { async beforeCancel() {
const paymentRefList = await this.getPayments(); await super.beforeCancel();
for (const paymentFor of paymentRefList) { const paymentIds = await this.getPaymentIds();
const paymentReference = paymentFor.parent; for (const paymentId of paymentIds) {
const payment = (await this.fyo.doc.getDoc( const paymentDoc = (await this.fyo.doc.getDoc(
'Payment', 'Payment',
paymentReference as string paymentId
)) as Payment; )) as Payment;
await paymentDoc.cancel();
const paymentEntries = await payment.getPosting();
for (const entry of paymentEntries) {
await entry.postReverse();
}
// To set the payment status as unsubmitted.
await this.fyo.db.update('Payment', {
name: paymentReference,
submitted: false,
cancelled: true,
});
} }
const entries = await this.getPosting(); }
await entries.postReverse();
async afterCancel() {
await super.afterCancel();
const partyDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.Party,
this.party!
)) as Party;
await partyDoc.updateOutstandingAmount();
}
async afterDelete() {
await super.afterDelete();
const paymentIds = await this.getPaymentIds();
for (const name of paymentIds) {
await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name);
}
}
async getPaymentIds() {
const payments = (await this.fyo.db.getAll('PaymentFor', {
fields: ['parent'],
filters: { referenceType: this.schemaName, referenceName: this.name! },
orderBy: 'name',
})) as { parent: string }[];
if (payments.length != 0) {
return [...new Set(payments.map(({ parent }) => parent))];
}
return [];
} }
async getExchangeRate() { async getExchangeRate() {

View File

@ -8,14 +8,15 @@ import {
} from 'fyo/model/types'; } from 'fyo/model/types';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { getLedgerLinkAction } from 'models/helpers'; import { getLedgerLinkAction } from 'models/helpers';
import { Transactional } from 'models/Transactional/Transactional';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { LedgerPosting } from '../../ledgerPosting/ledgerPosting'; import { LedgerPosting } from '../../Transactional/LedgerPosting';
export class JournalEntry extends Doc { export class JournalEntry extends Transactional {
accounts: Doc[] = []; accounts: Doc[] = [];
getPosting() { async getPosting() {
const entries = new LedgerPosting({ reference: this }, this.fyo); 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 debit = row.debit as Money;
@ -23,25 +24,13 @@ export class JournalEntry extends Doc {
const account = row.account as string; const account = row.account as string;
if (!debit.isZero()) { if (!debit.isZero()) {
entries.debit(account, debit); await posting.debit(account, debit);
} else if (!credit.isZero()) { } else if (!credit.isZero()) {
entries.credit(account, credit); await posting.credit(account, credit);
} }
} }
return entries; return posting;
}
async beforeSync() {
this.getPosting().validateEntries();
}
async beforeSubmit() {
await this.getPosting().post();
}
async afterCancel() {
await this.getPosting().postReverse();
} }
static defaults: DefaultMap = { static defaults: DefaultMap = {

View File

@ -15,39 +15,55 @@ import { PartyRole } from './types';
export class Party extends Doc { export class Party extends Doc {
async updateOutstandingAmount() { async updateOutstandingAmount() {
/**
* If Role === "Both" then outstanding Amount
* will be the amount to be paid to the party.
*/
const role = this.role as PartyRole; const role = this.role as PartyRole;
switch (role) { let outstandingAmount = this.fyo.pesa(0);
case 'Customer':
await this._updateOutstandingAmount('SalesInvoice'); if (role === 'Customer' || role === 'Both') {
break; const outstandingReceive = await this._getTotalOutstandingAmount(
case 'Supplier': 'SalesInvoice'
await this._updateOutstandingAmount('PurchaseInvoice'); );
break; outstandingAmount = outstandingAmount.add(outstandingReceive);
case 'Both':
await this._updateOutstandingAmount('SalesInvoice');
await this._updateOutstandingAmount('PurchaseInvoice');
break;
} }
if (role === 'Supplier') {
const outstandingPay = await this._getTotalOutstandingAmount(
'PurchaseInvoice'
);
outstandingAmount = outstandingAmount.add(outstandingPay);
}
if (role === 'Both') {
const outstandingPay = await this._getTotalOutstandingAmount(
'PurchaseInvoice'
);
outstandingAmount = outstandingAmount.sub(outstandingPay);
}
await this.setAndSync({ outstandingAmount });
} }
async _updateOutstandingAmount( async _getTotalOutstandingAmount(
schemaName: 'SalesInvoice' | 'PurchaseInvoice' schemaName: 'SalesInvoice' | 'PurchaseInvoice'
) { ) {
const outstandingAmounts = ( const outstandingAmounts = await this.fyo.db.getAllRaw(schemaName, {
await this.fyo.db.getAllRaw(schemaName, { fields: ['outstandingAmount'],
fields: ['outstandingAmount', 'party'], filters: {
filters: { submitted: true }, submitted: true,
}) cancelled: false,
).filter(({ party }) => party === this.name); party: this.name as string,
},
});
const totalOutstanding = outstandingAmounts return outstandingAmounts
.map(({ outstandingAmount }) => .map(({ outstandingAmount }) =>
this.fyo.pesa(outstandingAmount as number) this.fyo.pesa(outstandingAmount as number)
) )
.reduce((a, b) => a.add(b), this.fyo.pesa(0)); .reduce((a, b) => a.add(b), this.fyo.pesa(0));
await this.set('outstandingAmount', totalOutstanding);
await this.sync();
} }
formulas: FormulaMap = { formulas: FormulaMap = {

View File

@ -3,6 +3,7 @@ import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { import {
Action, Action,
ChangeArg,
DefaultMap, DefaultMap,
FiltersMap, FiltersMap,
FormulaMap, FormulaMap,
@ -13,26 +14,29 @@ import {
} from 'fyo/model/types'; } from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { getLedgerLinkAction } from 'models/helpers'; import { getLedgerLinkAction } from 'models/helpers';
import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { getIsNullOrUndef } from 'utils'; import { Invoice } from '../Invoice/Invoice';
import { Party } from '../Party/Party'; import { Party } from '../Party/Party';
import { PaymentFor } from '../PaymentFor/PaymentFor';
import { PaymentMethod, PaymentType } from './types'; import { PaymentMethod, PaymentType } from './types';
export class Payment extends Doc { export class Payment extends Transactional {
party?: string; party?: string;
amount?: Money; amount?: Money;
writeoff?: Money; writeoff?: Money;
paymentType?: PaymentType;
async change({ changed }: { changed: string }) { async change({ changed }: ChangeArg) {
switch (changed) { if (changed === 'for') {
case 'for': { this.updateAmountOnReferenceUpdate();
this.updateAmountOnReferenceUpdate(); await this.updateDetailsOnReferenceUpdate();
await this.updateDetailsOnReferenceUpdate(); }
}
case 'amount': { if (changed === 'amount') {
this.updateReferenceOnAmountUpdate(); this.updateReferenceOnAmountUpdate();
}
} }
} }
@ -51,20 +55,19 @@ export class Payment extends Doc {
} }
const schemaName = referenceType as string; const schemaName = referenceType as string;
const doc = await this.fyo.doc.getDoc(schemaName, referenceName as string); const doc = (await this.fyo.doc.getDoc(
schemaName,
referenceName as string
)) as Invoice;
let party;
let paymentType: PaymentType; let paymentType: PaymentType;
if (doc.isSales) {
if (schemaName === 'SalesInvoice') {
party = doc.customer;
paymentType = 'Receive'; paymentType = 'Receive';
} else { } else {
party = doc.supplier;
paymentType = 'Pay'; paymentType = 'Pay';
} }
this.party = party as string; this.party = doc.party as string;
this.paymentType = paymentType; this.paymentType = paymentType;
} }
@ -87,21 +90,26 @@ export class Payment extends Doc {
} }
async validate() { async validate() {
await super.validate();
this.validateAccounts(); this.validateAccounts();
this.validateReferenceAmount(); this.validateTotalReferenceAmount();
this.validateWriteOff(); this.validateWriteOffAccount();
await this.validateReferences();
} }
validateAccounts() { validateAccounts() {
if (this.paymentAccount !== this.account || !this.account) { if (this.paymentAccount !== this.account || !this.account) {
return; return;
} }
throw new this.fyo.errors.ValidationError( throw new this.fyo.errors.ValidationError(
`To Account and From Account can't be the same: ${this.account}` t`To Account and From Account can't be the same: ${
this.account as string
}`
); );
} }
validateReferenceAmount() { validateTotalReferenceAmount() {
const forReferences = (this.for ?? []) as Doc[]; const forReferences = (this.for ?? []) as Doc[];
if (forReferences.length === 0) { if (forReferences.length === 0) {
return; return;
@ -137,7 +145,7 @@ export class Payment extends Doc {
); );
} }
validateWriteOff() { validateWriteOffAccount() {
if ((this.writeoff as Money).isZero()) { if ((this.writeoff as Money).isZero()) {
return; return;
} }
@ -155,131 +163,134 @@ export class Payment extends Doc {
const paymentAccount = this.paymentAccount as string; const paymentAccount = this.paymentAccount as string;
const amount = this.amount as Money; const amount = this.amount as Money;
const writeoff = this.writeoff as Money; const writeoff = this.writeoff as Money;
const entries = new LedgerPosting( const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
{
reference: this,
party: this.party!,
},
this.fyo
);
await entries.debit(paymentAccount as string, amount.sub(writeoff)); await posting.debit(paymentAccount as string, amount.sub(writeoff));
await entries.credit(account as string, amount.sub(writeoff)); await posting.credit(account as string, amount.sub(writeoff));
this.applyWriteOffPosting(posting);
return posting;
}
async applyWriteOffPosting(posting: LedgerPosting) {
const writeoff = this.writeoff as Money;
if (writeoff.isZero()) { if (writeoff.isZero()) {
return [entries]; return posting;
} }
const writeoffEntry = new LedgerPosting( const account = this.account as string;
{
reference: this,
party: this.party!,
},
this.fyo
);
const writeOffAccount = this.fyo.singles.AccountingSettings! const writeOffAccount = this.fyo.singles.AccountingSettings!
.writeOffAccount as string; .writeOffAccount as string;
if (this.paymentType === 'Pay') { if (this.paymentType === 'Pay') {
await writeoffEntry.debit(account, writeoff); await posting.credit(account, writeoff);
await writeoffEntry.credit(writeOffAccount, writeoff); await posting.debit(writeOffAccount, writeoff);
} else { } else {
await writeoffEntry.debit(writeOffAccount, writeoff); await posting.debit(account, writeoff);
await writeoffEntry.credit(account, writeoff); await posting.credit(writeOffAccount, writeoff);
} }
return [entries, writeoffEntry];
} }
async beforeSubmit() { async validateReferences() {
const forReferences = (this.for ?? []) as Doc[]; const forReferences = (this.for ?? []) as PaymentFor[];
if (forReferences.length === 0) { if (forReferences.length === 0) {
return; return;
} }
for (const row of forReferences) { for (const row of forReferences) {
if ( this.validateReferenceType(row);
!['SalesInvoice', 'PurchaseInvoice'].includes( await this.validateReferenceOutstanding(row);
row.referenceType as string
)
) {
continue;
}
const referenceDoc = await this.fyo.doc.getDoc(
row.referenceType as string,
row.referenceName as string
);
let outstandingAmount = referenceDoc.outstandingAmount as Money;
const baseGrandTotal = referenceDoc.baseGrandTotal as Money;
const amount = this.amount as Money;
if (getIsNullOrUndef(outstandingAmount)) {
outstandingAmount = baseGrandTotal;
}
if (amount.lte(0) || amount.gt(outstandingAmount)) {
let message = this.fyo.t`Payment amount: ${this.fyo.format(
this.amount!,
'Currency'
)} should be less than Outstanding amount: ${this.fyo.format(
outstandingAmount,
'Currency'
)}.`;
if (amount.lte(0)) {
const amt = this.fyo.format(this.amount!, 'Currency');
message = this.fyo
.t`Payment amount: ${amt} should be greater than 0.`;
}
throw new ValidationError(message);
} else {
// update outstanding amounts in invoice and party
const newOutstanding = outstandingAmount.sub(amount);
await referenceDoc.set('outstandingAmount', newOutstanding);
await referenceDoc.sync();
const party = (await this.fyo.doc.getDoc(
'Party',
this.party!
)) as Party;
await party.updateOutstandingAmount();
}
} }
} }
async afterSubmit() { validateReferenceType(row: PaymentFor) {
const entryList = await this.getPosting(); const referenceType = row.referenceType;
for (const entry of entryList) { if (
await entry.post(); ![ModelNameEnum.SalesInvoice, ModelNameEnum.PurchaseInvoice].includes(
referenceType!
)
) {
throw new ValidationError(t`Please select a valid reference type.`);
}
}
async validateReferenceOutstanding(row: PaymentFor) {
const referenceDoc = await this.fyo.doc.getDoc(
row.referenceType as string,
row.referenceName as string
);
const outstandingAmount = referenceDoc.outstandingAmount as Money;
const amount = this.amount as Money;
if (amount.gt(0) && amount.lte(outstandingAmount)) {
return;
}
let message = this.fyo.t`Payment amount: ${this.fyo.format(
this.amount!,
'Currency'
)} should be less than Outstanding amount: ${this.fyo.format(
outstandingAmount,
'Currency'
)}.`;
if (amount.lte(0)) {
const amt = this.fyo.format(this.amount!, 'Currency');
message = this.fyo.t`Payment amount: ${amt} should be greater than 0.`;
}
throw new ValidationError(message);
}
async beforeSubmit() {
await this.updateReferenceDocOutstanding();
await this.updatePartyOutstanding();
}
async updateReferenceDocOutstanding() {
for (const row of (this.for ?? []) as PaymentFor[]) {
const referenceDoc = await this.fyo.doc.getDoc(
row.referenceType!,
row.referenceName!
);
const previousOutstandingAmount = referenceDoc.outstandingAmount as Money;
const outstandingAmount = previousOutstandingAmount.sub(this.amount!);
await referenceDoc.setAndSync({ outstandingAmount });
} }
} }
async afterCancel() { async afterCancel() {
this.updateReferenceOutstandingAmount(); await super.afterCancel();
const entryList = await this.getPosting(); this.revertOutstandingAmount();
for (const entry of entryList) { }
await entry.postReverse();
async revertOutstandingAmount() {
await this._revertReferenceOutstanding();
await this.updatePartyOutstanding();
}
async _revertReferenceOutstanding() {
for (const ref of this.for as PaymentFor[]) {
const refDoc = await this.fyo.doc.getDoc(
ref.referenceType!,
ref.referenceName!
);
const outstandingAmount = (refDoc.outstandingAmount as Money).add(
ref.amount!
);
await refDoc.setAndSync({ outstandingAmount });
} }
} }
async updateReferenceOutstandingAmount() { async updatePartyOutstanding() {
await (this.for as Doc[]).forEach( const partyDoc = (await this.fyo.doc.getDoc(
async ({ amount, referenceType, referenceName }) => { ModelNameEnum.Party,
const refDoc = await this.fyo.doc.getDoc( this.party!
referenceType as string, )) as Party;
referenceName as string await partyDoc.updateOutstandingAmount();
);
refDoc.setMultiple({
outstandingAmount: (refDoc.outstandingAmount as Money).add(
amount as Money
),
});
refDoc.sync();
}
);
} }
static defaults: DefaultMap = { date: () => new Date().toISOString() }; static defaults: DefaultMap = { date: () => new Date().toISOString() };
@ -334,11 +345,12 @@ export class Payment extends Doc {
required: RequiredMap = { required: RequiredMap = {
referenceId: () => this.paymentMethod !== 'Cash', referenceId: () => this.paymentMethod !== 'Cash',
clearanceDate: () => this.paymentMethod === 'Cash', clearanceDate: () => this.paymentMethod !== 'Cash',
}; };
hidden: HiddenMap = { hidden: HiddenMap = {
referenceId: () => this.paymentMethod !== 'Cash', referenceId: () => this.paymentMethod === 'Cash',
clearanceDate: () => this.paymentMethod === 'Cash',
}; };
static filters: FiltersMap = { static filters: FiltersMap = {

View File

@ -1,9 +1,41 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { FiltersMap, FormulaMap } from 'fyo/model/types'; import { FiltersMap, FormulaMap } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { PartyRoleEnum } from '../Party/types';
import { Payment } from '../Payment/Payment';
export class PaymentFor extends Doc { export class PaymentFor extends Doc {
parentdoc?: Payment | undefined;
referenceType?: ModelNameEnum.SalesInvoice | ModelNameEnum.PurchaseInvoice;
referenceName?: string;
amount?: Money;
formulas: FormulaMap = { formulas: FormulaMap = {
referenceType: {
formula: async () => {
if (this.referenceType) {
return;
}
const party = this.parentdoc!.party;
if (party === undefined) {
return ModelNameEnum.SalesInvoice;
}
const role = await this.fyo.getValue(
ModelNameEnum.Party,
party,
'role'
);
if (role === PartyRoleEnum.Supplier) {
return ModelNameEnum.PurchaseInvoice;
}
return ModelNameEnum.SalesInvoice;
},
},
amount: { amount: {
formula: async () => { formula: async () => {
if (!this.referenceName) { if (!this.referenceName) {
@ -27,8 +59,19 @@ export class PaymentFor extends Doc {
}; };
static filters: FiltersMap = { static filters: FiltersMap = {
referenceName: () => ({ referenceName: (doc) => {
outstandingAmount: ['>', 0], const baseFilters = {
}), outstandingAmount: ['>', 0],
submitted: true,
cancelled: false,
};
const party = doc?.parentdoc?.party as undefined | string;
if (party === undefined) {
return baseFilters;
}
return { ...baseFilters, party };
},
}; };
} }

View File

@ -1,6 +1,6 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { Action, ListViewSettings } from 'fyo/model/types'; import { Action, ListViewSettings } from 'fyo/model/types';
import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers'; import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
import { Invoice } from '../Invoice/Invoice'; import { Invoice } from '../Invoice/Invoice';
@ -10,28 +10,22 @@ export class PurchaseInvoice extends Invoice {
items?: PurchaseInvoiceItem[]; items?: PurchaseInvoiceItem[];
async getPosting() { async getPosting() {
const entries: LedgerPosting = new LedgerPosting( const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
{
reference: this,
party: this.party,
},
this.fyo
);
await entries.credit(this.account!, this.baseGrandTotal!); await posting.credit(this.account!, this.baseGrandTotal!);
for (const item of this.items!) { for (const item of this.items!) {
await entries.debit(item.account!, item.baseAmount!); await posting.debit(item.account!, item.baseAmount!);
} }
if (this.taxes) { if (this.taxes) {
for (const tax of this.taxes) { for (const tax of this.taxes) {
await entries.debit(tax.account!, tax.baseAmount!); await posting.debit(tax.account!, tax.baseAmount!);
} }
} }
entries.makeRoundOffEntry(); await posting.makeRoundOffEntry();
return entries; return posting;
} }
static getActions(fyo: Fyo): Action[] { static getActions(fyo: Fyo): Action[] {

View File

@ -1,6 +1,6 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { Action, ListViewSettings } from 'fyo/model/types'; import { Action, ListViewSettings } from 'fyo/model/types';
import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers'; import { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
import { Invoice } from '../Invoice/Invoice'; import { Invoice } from '../Invoice/Invoice';
@ -10,26 +10,21 @@ export class SalesInvoice extends Invoice {
items?: SalesInvoiceItem[]; items?: SalesInvoiceItem[];
async getPosting() { async getPosting() {
const entries: LedgerPosting = new LedgerPosting( const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
{ await posting.debit(this.account!, this.baseGrandTotal!);
reference: this,
party: this.party,
},
this.fyo
);
await entries.debit(this.account!, this.baseGrandTotal!);
for (const item of this.items!) { for (const item of this.items!) {
await entries.credit(item.account!, item.baseAmount!); await posting.credit(item.account!, item.baseAmount!);
} }
if (this.taxes) { if (this.taxes) {
for (const tax of this.taxes!) { for (const tax of this.taxes!) {
await entries.credit(tax.account!, tax.baseAmount!); await posting.credit(tax.account!, tax.baseAmount!);
} }
} }
entries.makeRoundOffEntry();
return entries; await posting.makeRoundOffEntry();
return posting;
} }
static getActions(fyo: Fyo): Action[] { static getActions(fyo: Fyo): Action[] {

View File

@ -7,26 +7,6 @@ import Money from 'pesa/dist/types/src/money';
import { Router } from 'vue-router'; import { Router } from 'vue-router';
import { InvoiceStatus, ModelNameEnum } from './types'; import { InvoiceStatus, ModelNameEnum } from './types';
export function getLedgerLinkAction(fyo: Fyo): Action {
return {
label: fyo.t`Ledger Entries`,
condition: (doc: Doc) => !!doc.submitted,
action: async (doc: Doc, router: Router) => {
router.push({
name: 'Report',
params: {
reportName: 'general-ledger',
defaultFilters: {
// @ts-ignore
referenceType: doc.schemaName,
referenceName: doc.name,
},
},
});
},
};
}
export function getInvoiceActions( export function getInvoiceActions(
schemaName: ModelNameEnum.PurchaseInvoice | ModelNameEnum.SalesInvoice, schemaName: ModelNameEnum.PurchaseInvoice | ModelNameEnum.SalesInvoice,
fyo: Fyo fyo: Fyo
@ -35,14 +15,15 @@ export function getInvoiceActions(
{ {
label: fyo.t`Make Payment`, label: fyo.t`Make Payment`,
condition: (doc: Doc) => condition: (doc: Doc) =>
(doc.submitted as boolean) && (doc.outstandingAmount as Money).gt(0), doc.isSubmitted && !(doc.outstandingAmount as Money).isZero(),
action: async function makePayment(doc: Doc) { action: async function makePayment(doc: Doc) {
const payment = await fyo.doc.getNewDoc('Payment'); const payment = await fyo.doc.getNewDoc('Payment');
payment.once('afterSync', async () => { payment.once('afterSync', async () => {
await payment.submit(); await payment.submit();
}); });
const isSales = schemaName === 'SalesInvoice'; const isSales = schemaName === 'SalesInvoice';
const party = isSales ? doc.customer : doc.supplier; const party = doc.party as string;
const paymentType = isSales ? 'Receive' : 'Pay'; const paymentType = isSales ? 'Receive' : 'Pay';
const hideAccountField = isSales ? 'account' : 'paymentAccount'; const hideAccountField = isSales ? 'account' : 'paymentAccount';
@ -71,6 +52,26 @@ export function getInvoiceActions(
]; ];
} }
export function getLedgerLinkAction(fyo: Fyo): Action {
return {
label: fyo.t`Ledger Entries`,
condition: (doc: Doc) => doc.isSubmitted,
action: async (doc: Doc, router: Router) => {
router.push({
name: 'Report',
params: {
reportName: 'general-ledger',
defaultFilters: {
// @ts-ignore
referenceType: doc.schemaName,
referenceName: doc.name,
},
},
});
},
};
}
export function getTransactionStatusColumn(): ColumnConfig { export function getTransactionStatusColumn(): ColumnConfig {
const statusMap = { const statusMap = {
Unpaid: t`Unpaid`, Unpaid: t`Unpaid`,

View File

@ -1,209 +0,0 @@
import { Fyo } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { ValidationError } from 'fyo/utils/errors';
import Money from 'pesa/dist/types/src/money';
import {
AccountEntry,
LedgerEntry,
LedgerPostingOptions,
TransactionType,
} from './types';
export class LedgerPosting {
reference: Doc;
party?: string;
date?: string;
description?: string;
entries: LedgerEntry[];
entryMap: Record<string, LedgerEntry>;
reverted: boolean;
accountEntries: AccountEntry[];
fyo: Fyo;
constructor(
{ reference, party, date, description }: LedgerPostingOptions,
fyo: Fyo
) {
this.reference = reference;
this.party = party;
this.date = date;
this.description = description ?? '';
this.entries = [];
this.entryMap = {};
this.reverted = false;
// To change balance while entering ledger entries
this.accountEntries = [];
this.fyo = fyo;
}
async debit(
account: string,
amount: Money,
referenceType?: string,
referenceName?: string
) {
const entry = this.getEntry(account, referenceType, referenceName);
entry.debit = entry.debit.add(amount);
await this.setAccountBalanceChange(account, 'debit', amount);
}
async credit(
account: string,
amount: Money,
referenceType?: string,
referenceName?: string
) {
const entry = this.getEntry(account, referenceType, referenceName);
entry.credit = entry.credit.add(amount);
await this.setAccountBalanceChange(account, 'credit', amount);
}
async setAccountBalanceChange(
accountName: string,
type: TransactionType,
amount: Money
) {
const debitAccounts = ['Asset', 'Expense'];
const accountDoc = await this.fyo.doc.getDoc('Account', accountName);
const rootType = accountDoc.rootType as string;
if (debitAccounts.indexOf(rootType) === -1) {
const change = type == 'credit' ? amount : amount.neg();
this.accountEntries.push({
name: accountName,
balanceChange: change,
});
} else {
const change = type == 'debit' ? amount : amount.neg();
this.accountEntries.push({
name: accountName,
balanceChange: change,
});
}
}
getEntry(account: string, referenceType?: string, referenceName?: string) {
if (!this.entryMap[account]) {
const entry: LedgerEntry = {
account: account,
party: this.party ?? '',
date: this.date ?? (this.reference.date as string),
referenceType: referenceType ?? this.reference.schemaName,
referenceName: referenceName ?? this.reference.name!,
description: this.description,
reverted: this.reverted,
debit: this.fyo.pesa(0),
credit: this.fyo.pesa(0),
};
this.entries.push(entry);
this.entryMap[account] = entry;
}
return this.entryMap[account];
}
async post() {
this.validateEntries();
await this.insertEntries();
}
async postReverse() {
this.validateEntries();
const data = await this.fyo.db.getAll('AccountingLedgerEntry', {
fields: ['name'],
filters: {
referenceName: this.reference.name!,
reverted: false,
},
});
for (const entry of data) {
const entryDoc = await this.fyo.doc.getDoc(
'AccountingLedgerEntry',
entry.name as string
);
entryDoc.reverted = true;
await entryDoc.sync();
}
let temp;
for (const entry of this.entries) {
temp = entry.debit;
entry.debit = entry.credit;
entry.credit = temp;
entry.reverted = true;
}
for (const entry of this.accountEntries) {
entry.balanceChange = (entry.balanceChange as Money).neg();
}
await this.insertEntries();
}
makeRoundOffEntry() {
const { debit, credit } = this.getTotalDebitAndCredit();
const difference = debit.sub(credit);
const absoluteValue = difference.abs();
const allowance = 0.5;
if (absoluteValue.eq(0)) {
return;
}
const roundOffAccount = this.getRoundOffAccount();
if (absoluteValue.lte(allowance)) {
if (difference.gt(0)) {
this.credit(roundOffAccount, absoluteValue);
} else {
this.debit(roundOffAccount, absoluteValue);
}
}
}
validateEntries() {
const { debit, credit } = this.getTotalDebitAndCredit();
if (debit.neq(credit)) {
throw new ValidationError(
`Total Debit: ${this.fyo.format(
debit,
'Currency'
)} must be equal to Total Credit: ${this.fyo.format(
credit,
'Currency'
)}`
);
}
}
getTotalDebitAndCredit() {
let debit = this.fyo.pesa(0);
let credit = this.fyo.pesa(0);
for (const entry of this.entries) {
debit = debit.add(entry.debit);
credit = credit.add(entry.credit);
}
return { debit, credit };
}
async insertEntries() {
for (const entry of this.entries) {
const entryDoc = this.fyo.doc.getNewDoc('AccountingLedgerEntry');
Object.assign(entryDoc, entry);
await entryDoc.sync();
}
for (const entry of this.accountEntries) {
const entryDoc = await this.fyo.doc.getDoc('Account', entry.name);
const balance = entryDoc.get('balance') as Money;
entryDoc.balance = balance.add(entry.balanceChange);
await entryDoc.sync();
}
}
getRoundOffAccount() {
return this.fyo.singles.AccountingSettings!.roundOffAccount as string;
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "AccountingLedgerEntry", "name": "AccountingLedgerEntry",
"label": "Ledger Entry", "label": "Accounting Ledger Entry",
"isSingle": false, "isSingle": false,
"isChild": false, "isChild": false,
"naming": "autoincrement", "naming": "autoincrement",
@ -8,52 +8,68 @@
{ {
"fieldname": "date", "fieldname": "date",
"label": "Date", "label": "Date",
"fieldtype": "Date" "fieldtype": "Date",
"readOnly": true
}, },
{ {
"fieldname": "account", "fieldname": "account",
"label": "Account", "label": "Account",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Account", "target": "Account",
"required": true "required": true,
"readOnly": true
}, },
{ {
"fieldname": "party", "fieldname": "party",
"label": "Party", "label": "Party",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Party" "target": "Party",
"readOnly": true
}, },
{ {
"fieldname": "debit", "fieldname": "debit",
"label": "Debit", "label": "Debit",
"fieldtype": "Currency" "fieldtype": "Currency",
"readOnly": true
}, },
{ {
"fieldname": "credit", "fieldname": "credit",
"label": "Credit", "label": "Credit",
"fieldtype": "Currency" "fieldtype": "Currency",
"readOnly": true
}, },
{ {
"fieldname": "referenceType", "fieldname": "referenceType",
"label": "Ref. Type", "label": "Ref. Type",
"fieldtype": "Data" "fieldtype": "Data",
"readOnly": true
}, },
{ {
"fieldname": "referenceName", "fieldname": "referenceName",
"label": "Ref. Name", "label": "Ref. Name",
"fieldtype": "DynamicLink", "fieldtype": "DynamicLink",
"references": "referenceType" "references": "referenceType",
"readOnly": true
}, },
{ {
"fieldname": "balance", "fieldname": "balance",
"label": "Balance", "label": "Balance",
"fieldtype": "Currency" "fieldtype": "Currency",
"readOnly": true
}, },
{ {
"fieldname": "reverted", "fieldname": "reverted",
"label": "Reverted", "label": "Reverted",
"fieldtype": "Check", "fieldtype": "Check",
"default": false "default": false,
"readOnly": true
},
{
"fieldname": "reverts",
"label": "Reverts",
"fieldtype": "Link",
"target": "AccountingLedgerEntry",
"readOnly": true
} }
], ],
"quickEditFields": [ "quickEditFields": [
@ -64,7 +80,9 @@
"credit", "credit",
"referenceType", "referenceType",
"referenceName", "referenceName",
"balance" "balance",
"reverted",
"reverts"
], ],
"keywordFields": ["account", "party", "referenceName"] "keywordFields": ["account", "party", "referenceName"]
} }

View File

@ -12,11 +12,11 @@
"options": [ "options": [
{ {
"value": "SalesInvoice", "value": "SalesInvoice",
"label": "Sales Invoice" "label": "Sales"
}, },
{ {
"value": "PurchaseInvoice", "value": "PurchaseInvoice",
"label": "Purchase Invoice" "label": "Purchase"
} }
], ],
"required": true "required": true

View File

@ -57,7 +57,7 @@
:class="inputClasses" :class="inputClasses"
:checked="value" :checked="value"
:readonly="isReadOnly" :readonly="isReadOnly"
@change="(e) => triggerChange(e.target.checked)" @change="(e) => !isReadOnly && triggerChange(e.target.checked)"
@focus="(e) => $emit('focus', e)" @focus="(e) => $emit('focus', e)"
/> />
</div> </div>

View File

@ -6,7 +6,7 @@ export default {
extends: Link, extends: Link,
created() { created() {
const watchKey = `doc.${this.df.references}`; const watchKey = `doc.${this.df.references}`;
this.targetWatcher = this.$watch(watchKey, function(newTarget, oldTarget) { this.targetWatcher = this.$watch(watchKey, function (newTarget, oldTarget) {
if (oldTarget && newTarget !== oldTarget) { if (oldTarget && newTarget !== oldTarget) {
this.triggerChange(''); this.triggerChange('');
} }
@ -16,13 +16,13 @@ export default {
this.targetWatcher(); this.targetWatcher();
}, },
methods: { methods: {
getTarget() { getTargetSchemaName() {
if (!this.doc) { if (!this.doc || !this.df.references) {
throw new Error('You must provide `doc` for DynamicLink to work.'); return null;
} }
return this.doc[this.df.references]; return this.doc[this.df.references];
} },
} },
}; };
</script> </script>

View File

@ -27,8 +27,15 @@ export default {
}, },
}, },
methods: { methods: {
getTargetSchemaName() {
return this.df.target;
},
async getSuggestions(keyword = '') { async getSuggestions(keyword = '') {
const schemaName = this.df.target; const schemaName = this.getTargetSchemaName();
if (!schemaName) {
return [];
}
const schema = fyo.schemaMap[schemaName]; const schema = fyo.schemaMap[schemaName];
const filters = await this.getFilters(keyword); const filters = await this.getFilters(keyword);

View File

@ -101,6 +101,7 @@ export default {
], ],
filters: { filters: {
party: this.doc.name, party: this.doc.name,
cancelled: false,
}, },
limit: 3, limit: 3,
orderBy: 'created', orderBy: 'created',

View File

@ -11,7 +11,7 @@
> >
{{ t`Print` }} {{ t`Print` }}
</Button> </Button>
<DropdownWithActions :actions="actions" /> <DropdownWithActions :actions="actions()" />
<Button <Button
v-if="doc?.notInserted || doc?.dirty" v-if="doc?.notInserted || doc?.dirty"
type="primary" type="primary"
@ -245,8 +245,8 @@ export default {
Button, Button,
FormControl, FormControl,
DropdownWithActions, DropdownWithActions,
Table Table,
}, },
provide() { provide() {
return { return {
schemaName: this.schemaName, schemaName: this.schemaName,
@ -267,9 +267,6 @@ export default {
address() { address() {
return this.printSettings && this.printSettings.getLink('address'); return this.printSettings && this.printSettings.getLink('address');
}, },
actions() {
return getActionsForDocument(this.doc);
},
}, },
async mounted() { async mounted() {
try { try {
@ -301,6 +298,9 @@ export default {
}, },
methods: { methods: {
routeTo, routeTo,
actions() {
return getActionsForDocument(this.doc);
},
getField(fieldname) { getField(fieldname) {
return fyo.getField(this.schemaName, fieldname); return fyo.getField(this.schemaName, fieldname);
}, },
@ -310,8 +310,8 @@ export default {
submit() { submit() {
const message = const message =
this.schemaName === ModelNameEnum.SalesInvoice this.schemaName === ModelNameEnum.SalesInvoice
? this.t`Are you sure you want to submit this Sales Invoice?` ? this.t`Submit Sales Invoice?`
: this.t`Are you sure you want to submit this Purchase Invoice?`; : this.t`Submit Purchase Invoice?`;
showMessageDialog({ showMessageDialog({
message, message,
buttons: [ buttons: [

View File

@ -161,9 +161,9 @@ import PageHeader from 'src/components/PageHeader';
import StatusBadge from 'src/components/StatusBadge'; import StatusBadge from 'src/components/StatusBadge';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { import {
getActionsForDocument, getActionsForDocument,
routeTo, routeTo,
showMessageDialog, showMessageDialog
} from 'src/utils/ui'; } from 'src/utils/ui';
import { handleErrorWithDialog } from '../errorHandling'; import { handleErrorWithDialog } from '../errorHandling';
@ -242,7 +242,7 @@ export default {
}, },
async submit() { async submit() {
showMessageDialog({ showMessageDialog({
message: this.t`Are you sure you want to submit this Journal Entry?`, message: this.t`Submit Journal Entry?`,
buttons: [ buttons: [
{ {
label: this.t`Yes`, label: this.t`Yes`,

View File

@ -66,9 +66,6 @@ export async function openQuickEdit({
showFields, showFields,
hideFields, hideFields,
defaults: stringifyCircular(defaults), defaults: stringifyCircular(defaults),
/*
lastRoute: currentRoute,
*/
}, },
}); });
} }
@ -142,12 +139,16 @@ export async function routeTo(route: string | RouteLocationRaw) {
} }
export function deleteDocWithPrompt(doc: Doc) { export function deleteDocWithPrompt(doc: Doc) {
const schemaLabel = fyo.schemaMap[doc.schemaName]!.label;
let detail = t`This action is permanent.`;
if (doc.isTransactional) {
detail = t`This action is permanent and will delete all ledger entries.`;
}
return new Promise((resolve) => { return new Promise((resolve) => {
showMessageDialog({ showMessageDialog({
message: t`Are you sure you want to delete ${ message: t`Delete ${schemaLabel} ${doc.name!}?`,
doc.schemaName detail,
} ${doc.name!}?`,
detail: t`This action is permanent`,
buttons: [ buttons: [
{ {
label: t`Delete`, label: t`Delete`,
@ -204,10 +205,9 @@ export async function cancelDocWithPrompt(doc: Doc) {
} }
return new Promise((resolve) => { return new Promise((resolve) => {
const schemaLabel = fyo.schemaMap[doc.schemaName]!.label;
showMessageDialog({ showMessageDialog({
message: t`Are you sure you want to cancel ${ message: t`Cancel ${schemaLabel} ${doc.name!}?`,
doc.schemaName
} ${doc.name!}?`,
detail, detail,
buttons: [ buttons: [
{ {
@ -276,7 +276,8 @@ function getDeleteAction(doc: Doc): Action {
template: '<span class="text-red-700">{{ t`Delete` }}</span>', template: '<span class="text-red-700">{{ t`Delete` }}</span>',
}, },
condition: (doc: Doc) => condition: (doc: Doc) =>
!doc.notInserted && !doc.submitted && !doc.schema.isSingle, (!doc.notInserted && !doc.schema.isSubmittable && !doc.schema.isSingle) ||
doc.isCancelled,
action: () => action: () =>
deleteDocWithPrompt(doc).then((res) => { deleteDocWithPrompt(doc).then((res) => {
if (res) { if (res) {