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:
parent
9a012207f1
commit
20ea214a4b
@ -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 ?? [])],
|
||||||
|
@ -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 = {};
|
||||||
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
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 {
|
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';
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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'],
|
||||||
|
@ -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.
|
async afterCancel() {
|
||||||
await this.fyo.db.update('Payment', {
|
await super.afterCancel();
|
||||||
name: paymentReference,
|
const partyDoc = (await this.fyo.doc.getDoc(
|
||||||
submitted: false,
|
ModelNameEnum.Party,
|
||||||
cancelled: true,
|
this.party!
|
||||||
});
|
)) as Party;
|
||||||
|
|
||||||
|
await partyDoc.updateOutstandingAmount();
|
||||||
}
|
}
|
||||||
const entries = await this.getPosting();
|
|
||||||
await entries.postReverse();
|
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() {
|
||||||
|
@ -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 = {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateOutstandingAmount(
|
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 _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 = {
|
||||||
|
@ -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,28 +14,31 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async updateDetailsOnReferenceUpdate() {
|
async updateDetailsOnReferenceUpdate() {
|
||||||
const forReferences = (this.for ?? []) as Doc[];
|
const forReferences = (this.for ?? []) as Doc[];
|
||||||
@ -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,70 +163,70 @@ 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));
|
||||||
|
|
||||||
if (writeoff.isZero()) {
|
this.applyWriteOffPosting(posting);
|
||||||
return [entries];
|
return posting;
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeoffEntry = new LedgerPosting(
|
async applyWriteOffPosting(posting: LedgerPosting) {
|
||||||
{
|
const writeoff = this.writeoff as Money;
|
||||||
reference: this,
|
if (writeoff.isZero()) {
|
||||||
party: this.party!,
|
return posting;
|
||||||
},
|
}
|
||||||
this.fyo
|
|
||||||
);
|
const account = this.account as string;
|
||||||
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 validateReferences() {
|
||||||
}
|
const forReferences = (this.for ?? []) as PaymentFor[];
|
||||||
|
|
||||||
async beforeSubmit() {
|
|
||||||
const forReferences = (this.for ?? []) as Doc[];
|
|
||||||
if (forReferences.length === 0) {
|
if (forReferences.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const row of forReferences) {
|
for (const row of forReferences) {
|
||||||
|
this.validateReferenceType(row);
|
||||||
|
await this.validateReferenceOutstanding(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validateReferenceType(row: PaymentFor) {
|
||||||
|
const referenceType = row.referenceType;
|
||||||
if (
|
if (
|
||||||
!['SalesInvoice', 'PurchaseInvoice'].includes(
|
![ModelNameEnum.SalesInvoice, ModelNameEnum.PurchaseInvoice].includes(
|
||||||
row.referenceType as string
|
referenceType!
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
continue;
|
throw new ValidationError(t`Please select a valid reference type.`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async validateReferenceOutstanding(row: PaymentFor) {
|
||||||
const referenceDoc = await this.fyo.doc.getDoc(
|
const referenceDoc = await this.fyo.doc.getDoc(
|
||||||
row.referenceType as string,
|
row.referenceType as string,
|
||||||
row.referenceName as string
|
row.referenceName as string
|
||||||
);
|
);
|
||||||
|
|
||||||
let outstandingAmount = referenceDoc.outstandingAmount as Money;
|
const outstandingAmount = referenceDoc.outstandingAmount as Money;
|
||||||
const baseGrandTotal = referenceDoc.baseGrandTotal as Money;
|
|
||||||
const amount = this.amount as Money;
|
const amount = this.amount as Money;
|
||||||
|
|
||||||
if (getIsNullOrUndef(outstandingAmount)) {
|
if (amount.gt(0) && amount.lte(outstandingAmount)) {
|
||||||
outstandingAmount = baseGrandTotal;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount.lte(0) || amount.gt(outstandingAmount)) {
|
|
||||||
let message = this.fyo.t`Payment amount: ${this.fyo.format(
|
let message = this.fyo.t`Payment amount: ${this.fyo.format(
|
||||||
this.amount!,
|
this.amount!,
|
||||||
'Currency'
|
'Currency'
|
||||||
@ -229,57 +237,60 @@ export class Payment extends Doc {
|
|||||||
|
|
||||||
if (amount.lte(0)) {
|
if (amount.lte(0)) {
|
||||||
const amt = this.fyo.format(this.amount!, 'Currency');
|
const amt = this.fyo.format(this.amount!, 'Currency');
|
||||||
message = this.fyo
|
message = this.fyo.t`Payment amount: ${amt} should be greater than 0.`;
|
||||||
.t`Payment amount: ${amt} should be greater than 0.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ValidationError(message);
|
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() {
|
async beforeSubmit() {
|
||||||
const entryList = await this.getPosting();
|
await this.updateReferenceDocOutstanding();
|
||||||
for (const entry of entryList) {
|
await this.updatePartyOutstanding();
|
||||||
await entry.post();
|
}
|
||||||
|
|
||||||
|
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 updateReferenceOutstandingAmount() {
|
async revertOutstandingAmount() {
|
||||||
await (this.for as Doc[]).forEach(
|
await this._revertReferenceOutstanding();
|
||||||
async ({ amount, referenceType, referenceName }) => {
|
await this.updatePartyOutstanding();
|
||||||
|
}
|
||||||
|
|
||||||
|
async _revertReferenceOutstanding() {
|
||||||
|
for (const ref of this.for as PaymentFor[]) {
|
||||||
const refDoc = await this.fyo.doc.getDoc(
|
const refDoc = await this.fyo.doc.getDoc(
|
||||||
referenceType as string,
|
ref.referenceType!,
|
||||||
referenceName as string
|
ref.referenceName!
|
||||||
);
|
);
|
||||||
|
|
||||||
refDoc.setMultiple({
|
const outstandingAmount = (refDoc.outstandingAmount as Money).add(
|
||||||
outstandingAmount: (refDoc.outstandingAmount as Money).add(
|
ref.amount!
|
||||||
amount as Money
|
|
||||||
),
|
|
||||||
});
|
|
||||||
refDoc.sync();
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
await refDoc.setAndSync({ outstandingAmount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() };
|
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 = {
|
||||||
|
@ -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) => {
|
||||||
|
const baseFilters = {
|
||||||
outstandingAmount: ['>', 0],
|
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 { 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[] {
|
||||||
|
@ -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[] {
|
||||||
|
@ -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`,
|
||||||
|
@ -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",
|
"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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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',
|
||||||
|
@ -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: [
|
||||||
|
@ -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`,
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user