mirror of
https://github.com/frappe/books.git
synced 2024-12-22 19:09:01 +00:00
incr: abstract away transactional
- add deletion - fix reversion
This commit is contained in:
parent
9a012207f1
commit
20ea214a4b
@ -234,6 +234,10 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
options: GetAllOptions = {}
|
||||
): Promise<FieldValueMap[]> {
|
||||
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 {
|
||||
fields = ['name', ...(schema.keywordFields ?? [])],
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
import { setName } from './naming';
|
||||
import {
|
||||
Action,
|
||||
ChangeArg,
|
||||
CurrenciesMap,
|
||||
DefaultMap,
|
||||
EmptyMessageMap,
|
||||
@ -37,7 +38,7 @@ import {
|
||||
ReadOnlyMap,
|
||||
RequiredMap,
|
||||
TreeViewSettings,
|
||||
ValidationMap
|
||||
ValidationMap,
|
||||
} from './types';
|
||||
import { validateOptions, validateRequired } from './validationFunction';
|
||||
|
||||
@ -106,6 +107,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return fieldnames.map((f) => this.fieldMap[f]);
|
||||
}
|
||||
|
||||
get isSubmitted() {
|
||||
return !!this.submitted && !this.cancelled;
|
||||
}
|
||||
|
||||
get isCancelled() {
|
||||
return !!this.submitted && !!this.cancelled;
|
||||
}
|
||||
|
||||
_setValuesWithoutChecks(data: DocValueMap) {
|
||||
for (const field of this.schema.fields) {
|
||||
const fieldname = field.fieldname;
|
||||
@ -131,7 +140,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
// set value and trigger change
|
||||
async set(
|
||||
fieldname: string | DocValueMap,
|
||||
value?: DocValue | Doc[]
|
||||
value?: DocValue | Doc[] | DocValueMap[]
|
||||
): Promise<boolean> {
|
||||
if (typeof fieldname === 'object') {
|
||||
return await this.setMultiple(fieldname as DocValueMap);
|
||||
@ -143,10 +152,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
this._setDirty(true);
|
||||
if (Array.isArray(value)) {
|
||||
this[fieldname] = value.map((row, i) => {
|
||||
row.idx = i;
|
||||
return row;
|
||||
});
|
||||
for (const row of value) {
|
||||
this.push(fieldname, row);
|
||||
}
|
||||
} else {
|
||||
const field = this.fieldMap[fieldname];
|
||||
await this._validateField(field, value);
|
||||
@ -177,7 +185,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return hasSet;
|
||||
}
|
||||
|
||||
_canSet(fieldname: string, value?: DocValue | Doc[]): boolean {
|
||||
_canSet(
|
||||
fieldname: string,
|
||||
value?: DocValue | Doc[] | DocValueMap[]
|
||||
): boolean {
|
||||
if (fieldname === 'numberSeries' && !this.notInserted) {
|
||||
return false;
|
||||
}
|
||||
@ -610,6 +621,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async delete() {
|
||||
if (this.schema.isSubmittable && !this.isCancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.trigger('beforeDelete');
|
||||
await this.fyo.db.delete(this.schemaName, this.name!);
|
||||
await this.trigger('afterDelete');
|
||||
@ -619,7 +634,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.schema.isSubmittable || this.cancelled) {
|
||||
if (!this.schema.isSubmittable || this.submitted || this.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -631,7 +646,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
if (!this.schema.isSubmittable || !this.submitted) {
|
||||
if (!this.schema.isSubmittable || !this.submitted || this.cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -727,14 +742,24 @@ export class Doc extends Observable<DocValue | 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 afterSync() {}
|
||||
async beforeDelete() {}
|
||||
async afterDelete() {}
|
||||
async beforeSubmit() {}
|
||||
async afterSubmit() {}
|
||||
async beforeRename() {}
|
||||
async afterRename() {}
|
||||
async beforeCancel() {}
|
||||
async afterCancel() {}
|
||||
async beforeDelete() {}
|
||||
async afterDelete() {}
|
||||
|
||||
formulas: FormulaMap = {};
|
||||
validations: ValidationMap = {};
|
||||
|
@ -35,6 +35,8 @@ export type CurrenciesMap = Record<string, GetCurrency | undefined>;
|
||||
export type HiddenMap = Record<string, Hidden | undefined>;
|
||||
export type ReadOnlyMap = Record<string, ReadOnly | undefined>;
|
||||
|
||||
export type ChangeArg = { doc: Doc; changed: string };
|
||||
|
||||
/**
|
||||
* Should add this for hidden too
|
||||
*/
|
||||
|
207
models/Transactional/LedgerPosting.ts
Normal file
207
models/Transactional/LedgerPosting.ts
Normal 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;
|
||||
}
|
||||
}
|
57
models/Transactional/Transactional.ts
Normal file
57
models/Transactional/Transactional.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,24 +4,22 @@ import Money from 'pesa/dist/types/src/money';
|
||||
export interface LedgerPostingOptions {
|
||||
reference: Doc;
|
||||
party?: string;
|
||||
date?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface LedgerEntry {
|
||||
account: string;
|
||||
party: string;
|
||||
date: string;
|
||||
referenceType: string;
|
||||
referenceName: string;
|
||||
description?: string;
|
||||
reverted: boolean;
|
||||
debit: Money;
|
||||
credit: Money;
|
||||
}
|
||||
|
||||
export interface AccountEntry {
|
||||
export interface AccountBalanceChange {
|
||||
name: string;
|
||||
balanceChange: Money;
|
||||
change: Money;
|
||||
}
|
||||
|
||||
export type TransactionType = 'credit' | 'debit';
|
@ -7,13 +7,25 @@ import {
|
||||
TreeViewSettings,
|
||||
} from 'fyo/model/types';
|
||||
import { QueryFilter } from 'utils/db/types';
|
||||
import { AccountRootType, AccountType } from './types';
|
||||
import { AccountRootType, AccountRootTypeEnum, AccountType } from './types';
|
||||
|
||||
export class Account extends Doc {
|
||||
rootType?: AccountRootType;
|
||||
accountType?: AccountType;
|
||||
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 = {
|
||||
/**
|
||||
* NestedSet indices are actually not used
|
||||
@ -55,7 +67,7 @@ export class Account extends Doc {
|
||||
}
|
||||
|
||||
static filters: FiltersMap = {
|
||||
parentAccount: (doc: Account) => {
|
||||
parentAccount: (doc: Doc) => {
|
||||
const filter: QueryFilter = {
|
||||
isGroup: true,
|
||||
};
|
||||
|
@ -1,7 +1,44 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { ListViewSettings } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
|
||||
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 {
|
||||
return {
|
||||
columns: ['account', 'party', 'debit', 'credit', 'balance'],
|
||||
|
@ -2,7 +2,7 @@ import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
|
||||
import { getExchangeRate } from 'models/helpers';
|
||||
import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { getIsNullOrUndef } from 'utils';
|
||||
@ -11,7 +11,7 @@ import { Payment } from '../Payment/Payment';
|
||||
import { Tax } from '../Tax/Tax';
|
||||
import { TaxSummary } from '../TaxSummary/TaxSummary';
|
||||
|
||||
export abstract class Invoice extends Doc {
|
||||
export abstract class Invoice extends Transactional {
|
||||
_taxes: Record<string, Tax> = {};
|
||||
taxes?: TaxSummary[];
|
||||
|
||||
@ -26,34 +26,12 @@ export abstract class Invoice extends Doc {
|
||||
submitted?: boolean;
|
||||
cancelled?: boolean;
|
||||
|
||||
abstract getPosting(): Promise<LedgerPosting>;
|
||||
|
||||
get isSales() {
|
||||
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() {
|
||||
// post ledger entries
|
||||
const entries = await this.getPosting();
|
||||
await entries.post();
|
||||
await super.afterSubmit();
|
||||
|
||||
// update outstanding amounts
|
||||
await this.fyo.db.update(this.schemaName, {
|
||||
@ -65,29 +43,48 @@ export abstract class Invoice extends Doc {
|
||||
await party.updateOutstandingAmount();
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
const paymentRefList = await this.getPayments();
|
||||
for (const paymentFor of paymentRefList) {
|
||||
const paymentReference = paymentFor.parent;
|
||||
const payment = (await this.fyo.doc.getDoc(
|
||||
async beforeCancel() {
|
||||
await super.beforeCancel();
|
||||
const paymentIds = await this.getPaymentIds();
|
||||
for (const paymentId of paymentIds) {
|
||||
const paymentDoc = (await this.fyo.doc.getDoc(
|
||||
'Payment',
|
||||
paymentReference as string
|
||||
paymentId
|
||||
)) as Payment;
|
||||
|
||||
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,
|
||||
});
|
||||
await paymentDoc.cancel();
|
||||
}
|
||||
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() {
|
||||
|
@ -8,14 +8,15 @@ import {
|
||||
} from 'fyo/model/types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getLedgerLinkAction } from 'models/helpers';
|
||||
import { Transactional } from 'models/Transactional/Transactional';
|
||||
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[] = [];
|
||||
|
||||
getPosting() {
|
||||
const entries = new LedgerPosting({ reference: this }, this.fyo);
|
||||
async getPosting() {
|
||||
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
|
||||
|
||||
for (const row of this.accounts) {
|
||||
const debit = row.debit as Money;
|
||||
@ -23,25 +24,13 @@ export class JournalEntry extends Doc {
|
||||
const account = row.account as string;
|
||||
|
||||
if (!debit.isZero()) {
|
||||
entries.debit(account, debit);
|
||||
await posting.debit(account, debit);
|
||||
} else if (!credit.isZero()) {
|
||||
entries.credit(account, credit);
|
||||
await posting.credit(account, credit);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async beforeSync() {
|
||||
this.getPosting().validateEntries();
|
||||
}
|
||||
|
||||
async beforeSubmit() {
|
||||
await this.getPosting().post();
|
||||
}
|
||||
|
||||
async afterCancel() {
|
||||
await this.getPosting().postReverse();
|
||||
return posting;
|
||||
}
|
||||
|
||||
static defaults: DefaultMap = {
|
||||
|
@ -15,39 +15,55 @@ import { PartyRole } from './types';
|
||||
|
||||
export class Party extends Doc {
|
||||
async updateOutstandingAmount() {
|
||||
/**
|
||||
* If Role === "Both" then outstanding Amount
|
||||
* will be the amount to be paid to the party.
|
||||
*/
|
||||
|
||||
const role = this.role as PartyRole;
|
||||
switch (role) {
|
||||
case 'Customer':
|
||||
await this._updateOutstandingAmount('SalesInvoice');
|
||||
break;
|
||||
case 'Supplier':
|
||||
await this._updateOutstandingAmount('PurchaseInvoice');
|
||||
break;
|
||||
case 'Both':
|
||||
await this._updateOutstandingAmount('SalesInvoice');
|
||||
await this._updateOutstandingAmount('PurchaseInvoice');
|
||||
break;
|
||||
let outstandingAmount = this.fyo.pesa(0);
|
||||
|
||||
if (role === 'Customer' || role === 'Both') {
|
||||
const outstandingReceive = await this._getTotalOutstandingAmount(
|
||||
'SalesInvoice'
|
||||
);
|
||||
outstandingAmount = outstandingAmount.add(outstandingReceive);
|
||||
}
|
||||
|
||||
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'
|
||||
) {
|
||||
const outstandingAmounts = (
|
||||
await this.fyo.db.getAllRaw(schemaName, {
|
||||
fields: ['outstandingAmount', 'party'],
|
||||
filters: { submitted: true },
|
||||
})
|
||||
).filter(({ party }) => party === this.name);
|
||||
const outstandingAmounts = await this.fyo.db.getAllRaw(schemaName, {
|
||||
fields: ['outstandingAmount'],
|
||||
filters: {
|
||||
submitted: true,
|
||||
cancelled: false,
|
||||
party: this.name as string,
|
||||
},
|
||||
});
|
||||
|
||||
const totalOutstanding = outstandingAmounts
|
||||
return outstandingAmounts
|
||||
.map(({ outstandingAmount }) =>
|
||||
this.fyo.pesa(outstandingAmount as number)
|
||||
)
|
||||
.reduce((a, b) => a.add(b), this.fyo.pesa(0));
|
||||
|
||||
await this.set('outstandingAmount', totalOutstanding);
|
||||
await this.sync();
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
|
@ -3,6 +3,7 @@ import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
Action,
|
||||
ChangeArg,
|
||||
DefaultMap,
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
@ -13,26 +14,29 @@ import {
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
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 { getIsNullOrUndef } from 'utils';
|
||||
import { Invoice } from '../Invoice/Invoice';
|
||||
import { Party } from '../Party/Party';
|
||||
import { PaymentFor } from '../PaymentFor/PaymentFor';
|
||||
import { PaymentMethod, PaymentType } from './types';
|
||||
|
||||
export class Payment extends Doc {
|
||||
export class Payment extends Transactional {
|
||||
party?: string;
|
||||
amount?: Money;
|
||||
writeoff?: Money;
|
||||
paymentType?: PaymentType;
|
||||
|
||||
async change({ changed }: { changed: string }) {
|
||||
switch (changed) {
|
||||
case 'for': {
|
||||
this.updateAmountOnReferenceUpdate();
|
||||
await this.updateDetailsOnReferenceUpdate();
|
||||
}
|
||||
case 'amount': {
|
||||
this.updateReferenceOnAmountUpdate();
|
||||
}
|
||||
async change({ changed }: ChangeArg) {
|
||||
if (changed === 'for') {
|
||||
this.updateAmountOnReferenceUpdate();
|
||||
await this.updateDetailsOnReferenceUpdate();
|
||||
}
|
||||
|
||||
if (changed === 'amount') {
|
||||
this.updateReferenceOnAmountUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,20 +55,19 @@ export class Payment extends Doc {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (schemaName === 'SalesInvoice') {
|
||||
party = doc.customer;
|
||||
if (doc.isSales) {
|
||||
paymentType = 'Receive';
|
||||
} else {
|
||||
party = doc.supplier;
|
||||
paymentType = 'Pay';
|
||||
}
|
||||
|
||||
this.party = party as string;
|
||||
this.party = doc.party as string;
|
||||
this.paymentType = paymentType;
|
||||
}
|
||||
|
||||
@ -87,21 +90,26 @@ export class Payment extends Doc {
|
||||
}
|
||||
|
||||
async validate() {
|
||||
await super.validate();
|
||||
this.validateAccounts();
|
||||
this.validateReferenceAmount();
|
||||
this.validateWriteOff();
|
||||
this.validateTotalReferenceAmount();
|
||||
this.validateWriteOffAccount();
|
||||
await this.validateReferences();
|
||||
}
|
||||
|
||||
validateAccounts() {
|
||||
if (this.paymentAccount !== this.account || !this.account) {
|
||||
return;
|
||||
}
|
||||
|
||||
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[];
|
||||
if (forReferences.length === 0) {
|
||||
return;
|
||||
@ -137,7 +145,7 @@ export class Payment extends Doc {
|
||||
);
|
||||
}
|
||||
|
||||
validateWriteOff() {
|
||||
validateWriteOffAccount() {
|
||||
if ((this.writeoff as Money).isZero()) {
|
||||
return;
|
||||
}
|
||||
@ -155,131 +163,134 @@ export class Payment extends Doc {
|
||||
const paymentAccount = this.paymentAccount as string;
|
||||
const amount = this.amount as Money;
|
||||
const writeoff = this.writeoff as Money;
|
||||
const entries = new LedgerPosting(
|
||||
{
|
||||
reference: this,
|
||||
party: this.party!,
|
||||
},
|
||||
this.fyo
|
||||
);
|
||||
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
|
||||
|
||||
await entries.debit(paymentAccount as string, amount.sub(writeoff));
|
||||
await entries.credit(account as string, amount.sub(writeoff));
|
||||
await posting.debit(paymentAccount 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()) {
|
||||
return [entries];
|
||||
return posting;
|
||||
}
|
||||
|
||||
const writeoffEntry = new LedgerPosting(
|
||||
{
|
||||
reference: this,
|
||||
party: this.party!,
|
||||
},
|
||||
this.fyo
|
||||
);
|
||||
const account = this.account as string;
|
||||
const writeOffAccount = this.fyo.singles.AccountingSettings!
|
||||
.writeOffAccount as string;
|
||||
|
||||
if (this.paymentType === 'Pay') {
|
||||
await writeoffEntry.debit(account, writeoff);
|
||||
await writeoffEntry.credit(writeOffAccount, writeoff);
|
||||
await posting.credit(account, writeoff);
|
||||
await posting.debit(writeOffAccount, writeoff);
|
||||
} else {
|
||||
await writeoffEntry.debit(writeOffAccount, writeoff);
|
||||
await writeoffEntry.credit(account, writeoff);
|
||||
await posting.debit(account, writeoff);
|
||||
await posting.credit(writeOffAccount, writeoff);
|
||||
}
|
||||
|
||||
return [entries, writeoffEntry];
|
||||
}
|
||||
|
||||
async beforeSubmit() {
|
||||
const forReferences = (this.for ?? []) as Doc[];
|
||||
async validateReferences() {
|
||||
const forReferences = (this.for ?? []) as PaymentFor[];
|
||||
if (forReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const row of forReferences) {
|
||||
if (
|
||||
!['SalesInvoice', 'PurchaseInvoice'].includes(
|
||||
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();
|
||||
}
|
||||
this.validateReferenceType(row);
|
||||
await this.validateReferenceOutstanding(row);
|
||||
}
|
||||
}
|
||||
|
||||
async afterSubmit() {
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.post();
|
||||
validateReferenceType(row: PaymentFor) {
|
||||
const referenceType = row.referenceType;
|
||||
if (
|
||||
![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() {
|
||||
this.updateReferenceOutstandingAmount();
|
||||
const entryList = await this.getPosting();
|
||||
for (const entry of entryList) {
|
||||
await entry.postReverse();
|
||||
await super.afterCancel();
|
||||
this.revertOutstandingAmount();
|
||||
}
|
||||
|
||||
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() {
|
||||
await (this.for as Doc[]).forEach(
|
||||
async ({ amount, referenceType, referenceName }) => {
|
||||
const refDoc = await this.fyo.doc.getDoc(
|
||||
referenceType as string,
|
||||
referenceName as string
|
||||
);
|
||||
|
||||
refDoc.setMultiple({
|
||||
outstandingAmount: (refDoc.outstandingAmount as Money).add(
|
||||
amount as Money
|
||||
),
|
||||
});
|
||||
refDoc.sync();
|
||||
}
|
||||
);
|
||||
async updatePartyOutstanding() {
|
||||
const partyDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party!
|
||||
)) as Party;
|
||||
await partyDoc.updateOutstandingAmount();
|
||||
}
|
||||
|
||||
static defaults: DefaultMap = { date: () => new Date().toISOString() };
|
||||
@ -334,11 +345,12 @@ export class Payment extends Doc {
|
||||
|
||||
required: RequiredMap = {
|
||||
referenceId: () => this.paymentMethod !== 'Cash',
|
||||
clearanceDate: () => this.paymentMethod === 'Cash',
|
||||
clearanceDate: () => this.paymentMethod !== 'Cash',
|
||||
};
|
||||
|
||||
hidden: HiddenMap = {
|
||||
referenceId: () => this.paymentMethod !== 'Cash',
|
||||
referenceId: () => this.paymentMethod === 'Cash',
|
||||
clearanceDate: () => this.paymentMethod === 'Cash',
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
|
@ -1,9 +1,41 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { FiltersMap, FormulaMap } from 'fyo/model/types';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Money from 'pesa/dist/types/src/money';
|
||||
import { PartyRoleEnum } from '../Party/types';
|
||||
import { Payment } from '../Payment/Payment';
|
||||
|
||||
export class PaymentFor extends Doc {
|
||||
parentdoc?: Payment | undefined;
|
||||
referenceType?: ModelNameEnum.SalesInvoice | ModelNameEnum.PurchaseInvoice;
|
||||
referenceName?: string;
|
||||
amount?: Money;
|
||||
|
||||
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: {
|
||||
formula: async () => {
|
||||
if (!this.referenceName) {
|
||||
@ -27,8 +59,19 @@ export class PaymentFor extends Doc {
|
||||
};
|
||||
|
||||
static filters: FiltersMap = {
|
||||
referenceName: () => ({
|
||||
outstandingAmount: ['>', 0],
|
||||
}),
|
||||
referenceName: (doc) => {
|
||||
const baseFilters = {
|
||||
outstandingAmount: ['>', 0],
|
||||
submitted: true,
|
||||
cancelled: false,
|
||||
};
|
||||
|
||||
const party = doc?.parentdoc?.party as undefined | string;
|
||||
if (party === undefined) {
|
||||
return baseFilters;
|
||||
}
|
||||
|
||||
return { ...baseFilters, party };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Fyo } from 'fyo';
|
||||
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 { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
|
||||
import { Invoice } from '../Invoice/Invoice';
|
||||
@ -10,28 +10,22 @@ export class PurchaseInvoice extends Invoice {
|
||||
items?: PurchaseInvoiceItem[];
|
||||
|
||||
async getPosting() {
|
||||
const entries: LedgerPosting = new LedgerPosting(
|
||||
{
|
||||
reference: this,
|
||||
party: this.party,
|
||||
},
|
||||
this.fyo
|
||||
);
|
||||
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
|
||||
|
||||
await entries.credit(this.account!, this.baseGrandTotal!);
|
||||
await posting.credit(this.account!, this.baseGrandTotal!);
|
||||
|
||||
for (const item of this.items!) {
|
||||
await entries.debit(item.account!, item.baseAmount!);
|
||||
await posting.debit(item.account!, item.baseAmount!);
|
||||
}
|
||||
|
||||
if (this.taxes) {
|
||||
for (const tax of this.taxes) {
|
||||
await entries.debit(tax.account!, tax.baseAmount!);
|
||||
await posting.debit(tax.account!, tax.baseAmount!);
|
||||
}
|
||||
}
|
||||
|
||||
entries.makeRoundOffEntry();
|
||||
return entries;
|
||||
await posting.makeRoundOffEntry();
|
||||
return posting;
|
||||
}
|
||||
|
||||
static getActions(fyo: Fyo): Action[] {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Fyo } from 'fyo';
|
||||
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 { getInvoiceActions, getTransactionStatusColumn } from '../../helpers';
|
||||
import { Invoice } from '../Invoice/Invoice';
|
||||
@ -10,26 +10,21 @@ export class SalesInvoice extends Invoice {
|
||||
items?: SalesInvoiceItem[];
|
||||
|
||||
async getPosting() {
|
||||
const entries: LedgerPosting = new LedgerPosting(
|
||||
{
|
||||
reference: this,
|
||||
party: this.party,
|
||||
},
|
||||
this.fyo
|
||||
);
|
||||
await entries.debit(this.account!, this.baseGrandTotal!);
|
||||
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
|
||||
await posting.debit(this.account!, this.baseGrandTotal!);
|
||||
|
||||
for (const item of this.items!) {
|
||||
await entries.credit(item.account!, item.baseAmount!);
|
||||
await posting.credit(item.account!, item.baseAmount!);
|
||||
}
|
||||
|
||||
if (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[] {
|
||||
|
@ -7,26 +7,6 @@ import Money from 'pesa/dist/types/src/money';
|
||||
import { Router } from 'vue-router';
|
||||
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(
|
||||
schemaName: ModelNameEnum.PurchaseInvoice | ModelNameEnum.SalesInvoice,
|
||||
fyo: Fyo
|
||||
@ -35,14 +15,15 @@ export function getInvoiceActions(
|
||||
{
|
||||
label: fyo.t`Make Payment`,
|
||||
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) {
|
||||
const payment = await fyo.doc.getNewDoc('Payment');
|
||||
payment.once('afterSync', async () => {
|
||||
await payment.submit();
|
||||
});
|
||||
|
||||
const isSales = schemaName === 'SalesInvoice';
|
||||
const party = isSales ? doc.customer : doc.supplier;
|
||||
const party = doc.party as string;
|
||||
const paymentType = isSales ? 'Receive' : 'Pay';
|
||||
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 {
|
||||
const statusMap = {
|
||||
Unpaid: t`Unpaid`,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "AccountingLedgerEntry",
|
||||
"label": "Ledger Entry",
|
||||
"label": "Accounting Ledger Entry",
|
||||
"isSingle": false,
|
||||
"isChild": false,
|
||||
"naming": "autoincrement",
|
||||
@ -8,52 +8,68 @@
|
||||
{
|
||||
"fieldname": "date",
|
||||
"label": "Date",
|
||||
"fieldtype": "Date"
|
||||
"fieldtype": "Date",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": "Account",
|
||||
"fieldtype": "Link",
|
||||
"target": "Account",
|
||||
"required": true
|
||||
"required": true,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"label": "Party",
|
||||
"fieldtype": "Link",
|
||||
"target": "Party"
|
||||
"target": "Party",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "debit",
|
||||
"label": "Debit",
|
||||
"fieldtype": "Currency"
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "credit",
|
||||
"label": "Credit",
|
||||
"fieldtype": "Currency"
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "referenceType",
|
||||
"label": "Ref. Type",
|
||||
"fieldtype": "Data"
|
||||
"fieldtype": "Data",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "referenceName",
|
||||
"label": "Ref. Name",
|
||||
"fieldtype": "DynamicLink",
|
||||
"references": "referenceType"
|
||||
"references": "referenceType",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "balance",
|
||||
"label": "Balance",
|
||||
"fieldtype": "Currency"
|
||||
"fieldtype": "Currency",
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "reverted",
|
||||
"label": "Reverted",
|
||||
"fieldtype": "Check",
|
||||
"default": false
|
||||
"default": false,
|
||||
"readOnly": true
|
||||
},
|
||||
{
|
||||
"fieldname": "reverts",
|
||||
"label": "Reverts",
|
||||
"fieldtype": "Link",
|
||||
"target": "AccountingLedgerEntry",
|
||||
"readOnly": true
|
||||
}
|
||||
],
|
||||
"quickEditFields": [
|
||||
@ -64,7 +80,9 @@
|
||||
"credit",
|
||||
"referenceType",
|
||||
"referenceName",
|
||||
"balance"
|
||||
"balance",
|
||||
"reverted",
|
||||
"reverts"
|
||||
],
|
||||
"keywordFields": ["account", "party", "referenceName"]
|
||||
}
|
||||
|
@ -12,11 +12,11 @@
|
||||
"options": [
|
||||
{
|
||||
"value": "SalesInvoice",
|
||||
"label": "Sales Invoice"
|
||||
"label": "Sales"
|
||||
},
|
||||
{
|
||||
"value": "PurchaseInvoice",
|
||||
"label": "Purchase Invoice"
|
||||
"label": "Purchase"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
|
@ -57,7 +57,7 @@
|
||||
:class="inputClasses"
|
||||
:checked="value"
|
||||
:readonly="isReadOnly"
|
||||
@change="(e) => triggerChange(e.target.checked)"
|
||||
@change="(e) => !isReadOnly && triggerChange(e.target.checked)"
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@ export default {
|
||||
extends: Link,
|
||||
created() {
|
||||
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) {
|
||||
this.triggerChange('');
|
||||
}
|
||||
@ -16,13 +16,13 @@ export default {
|
||||
this.targetWatcher();
|
||||
},
|
||||
methods: {
|
||||
getTarget() {
|
||||
if (!this.doc) {
|
||||
throw new Error('You must provide `doc` for DynamicLink to work.');
|
||||
getTargetSchemaName() {
|
||||
if (!this.doc || !this.df.references) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.doc[this.df.references];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -27,8 +27,15 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getTargetSchemaName() {
|
||||
return this.df.target;
|
||||
},
|
||||
async getSuggestions(keyword = '') {
|
||||
const schemaName = this.df.target;
|
||||
const schemaName = this.getTargetSchemaName();
|
||||
if (!schemaName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const schema = fyo.schemaMap[schemaName];
|
||||
const filters = await this.getFilters(keyword);
|
||||
|
||||
|
@ -101,6 +101,7 @@ export default {
|
||||
],
|
||||
filters: {
|
||||
party: this.doc.name,
|
||||
cancelled: false,
|
||||
},
|
||||
limit: 3,
|
||||
orderBy: 'created',
|
||||
|
@ -11,7 +11,7 @@
|
||||
>
|
||||
{{ t`Print` }}
|
||||
</Button>
|
||||
<DropdownWithActions :actions="actions" />
|
||||
<DropdownWithActions :actions="actions()" />
|
||||
<Button
|
||||
v-if="doc?.notInserted || doc?.dirty"
|
||||
type="primary"
|
||||
@ -245,8 +245,8 @@ export default {
|
||||
Button,
|
||||
FormControl,
|
||||
DropdownWithActions,
|
||||
Table
|
||||
},
|
||||
Table,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
schemaName: this.schemaName,
|
||||
@ -267,9 +267,6 @@ export default {
|
||||
address() {
|
||||
return this.printSettings && this.printSettings.getLink('address');
|
||||
},
|
||||
actions() {
|
||||
return getActionsForDocument(this.doc);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
@ -301,6 +298,9 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
routeTo,
|
||||
actions() {
|
||||
return getActionsForDocument(this.doc);
|
||||
},
|
||||
getField(fieldname) {
|
||||
return fyo.getField(this.schemaName, fieldname);
|
||||
},
|
||||
@ -310,8 +310,8 @@ export default {
|
||||
submit() {
|
||||
const message =
|
||||
this.schemaName === ModelNameEnum.SalesInvoice
|
||||
? this.t`Are you sure you want to submit this Sales Invoice?`
|
||||
: this.t`Are you sure you want to submit this Purchase Invoice?`;
|
||||
? this.t`Submit Sales Invoice?`
|
||||
: this.t`Submit Purchase Invoice?`;
|
||||
showMessageDialog({
|
||||
message,
|
||||
buttons: [
|
||||
|
@ -161,9 +161,9 @@ import PageHeader from 'src/components/PageHeader';
|
||||
import StatusBadge from 'src/components/StatusBadge';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import {
|
||||
getActionsForDocument,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
getActionsForDocument,
|
||||
routeTo,
|
||||
showMessageDialog
|
||||
} from 'src/utils/ui';
|
||||
import { handleErrorWithDialog } from '../errorHandling';
|
||||
|
||||
@ -242,7 +242,7 @@ export default {
|
||||
},
|
||||
async submit() {
|
||||
showMessageDialog({
|
||||
message: this.t`Are you sure you want to submit this Journal Entry?`,
|
||||
message: this.t`Submit Journal Entry?`,
|
||||
buttons: [
|
||||
{
|
||||
label: this.t`Yes`,
|
||||
|
@ -66,9 +66,6 @@ export async function openQuickEdit({
|
||||
showFields,
|
||||
hideFields,
|
||||
defaults: stringifyCircular(defaults),
|
||||
/*
|
||||
lastRoute: currentRoute,
|
||||
*/
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -142,12 +139,16 @@ export async function routeTo(route: string | RouteLocationRaw) {
|
||||
}
|
||||
|
||||
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) => {
|
||||
showMessageDialog({
|
||||
message: t`Are you sure you want to delete ${
|
||||
doc.schemaName
|
||||
} ${doc.name!}?`,
|
||||
detail: t`This action is permanent`,
|
||||
message: t`Delete ${schemaLabel} ${doc.name!}?`,
|
||||
detail,
|
||||
buttons: [
|
||||
{
|
||||
label: t`Delete`,
|
||||
@ -204,10 +205,9 @@ export async function cancelDocWithPrompt(doc: Doc) {
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const schemaLabel = fyo.schemaMap[doc.schemaName]!.label;
|
||||
showMessageDialog({
|
||||
message: t`Are you sure you want to cancel ${
|
||||
doc.schemaName
|
||||
} ${doc.name!}?`,
|
||||
message: t`Cancel ${schemaLabel} ${doc.name!}?`,
|
||||
detail,
|
||||
buttons: [
|
||||
{
|
||||
@ -276,7 +276,8 @@ function getDeleteAction(doc: Doc): Action {
|
||||
template: '<span class="text-red-700">{{ t`Delete` }}</span>',
|
||||
},
|
||||
condition: (doc: Doc) =>
|
||||
!doc.notInserted && !doc.submitted && !doc.schema.isSingle,
|
||||
(!doc.notInserted && !doc.schema.isSubmittable && !doc.schema.isSingle) ||
|
||||
doc.isCancelled,
|
||||
action: () =>
|
||||
deleteDocWithPrompt(doc).then((res) => {
|
||||
if (res) {
|
||||
|
Loading…
Reference in New Issue
Block a user