Compare commits

...

6 Commits

Author SHA1 Message Date
allcontributors[bot]
44bd916ede docs: update .all-contributorsrc [skip ci] 2023-10-25 20:14:18 +00:00
allcontributors[bot]
fd5db4f055 docs: update README.md [skip ci] 2023-10-25 20:14:17 +00:00
Robert Koch
e4a7f09dbc feat: Add tax numbers to the organization details (#269) 2023-10-25 22:10:46 +02:00
Ahmed Bouhuolia
2c5537efad fix: Trial balance sheet adjusted balance (#273) 2023-10-25 13:18:13 +02:00
Ahmed Bouhuolia
017908600e feat: improve financial statements rows color (#276) 2023-10-24 22:40:54 +02:00
dependabot[bot]
6307ca8935 chore(deps-dev): bump @babel/traverse in /packages/server (#272)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.23.0 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-19 19:33:12 +02:00
28 changed files with 1033 additions and 584 deletions

View File

@@ -60,6 +60,15 @@
"contributions": [ "contributions": [
"bug" "bug"
] ]
},
{
"login": "kochie",
"name": "Robert Koch",
"avatar_url": "https://avatars.githubusercontent.com/u/10809884?v=4",
"profile": "https://me.kochie.io",
"contributions": [
"code"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

View File

@@ -80,6 +80,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td> <td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
/** /**
* Router constructor. * Router constructor.
*/ */
router() { public router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -36,7 +36,7 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
* Validation schema. * Validation schema.
* @return {ValidationChain[]} * @return {ValidationChain[]}
*/ */
get trialBalanceSheetValidationSchema(): ValidationChain[] { private get trialBalanceSheetValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('basis').optional(), query('basis').optional(),
@@ -59,28 +59,37 @@ export default class TrialBalanceSheetController extends BaseFinancialReportCont
/** /**
* Retrieve the trial balance sheet. * Retrieve the trial balance sheet.
*/ */
public async trialBalanceSheet( private async trialBalanceSheet(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId, settings } = req; const { tenantId } = req;
let filter = this.matchedQueryData(req); let filter = this.matchedQueryData(req);
filter = { filter = {
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
try { try {
const { data, query, meta } = const accept = this.accepts(req);
await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter); const acceptType = accept.types(['json', 'application/json+table']);
return res.status(200).send({ if (acceptType === 'application/json+table') {
data: this.transfromToResponse(data), const { table, meta, query } =
query: this.transfromToResponse(query), await this.trialBalanceSheetService.trialBalanceSheetTable(
meta: this.transfromToResponse(meta), tenantId,
}); filter
);
return res.status(200).send({ table, meta, query });
} else {
const { data, query, meta } =
await this.trialBalanceSheetService.trialBalanceSheet(
tenantId,
filter
);
return res.status(200).send({ data, query, meta });
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -31,14 +31,14 @@ export default class OrganizationController extends BaseController {
router.post( router.post(
'/build', '/build',
this.organizationValidationSchema, this.buildOrganizationValidationSchema,
this.validationResult, this.validationResult,
asyncMiddleware(this.build.bind(this)), asyncMiddleware(this.build.bind(this)),
this.handleServiceErrors.bind(this) this.handleServiceErrors.bind(this)
); );
router.put( router.put(
'/', '/',
this.organizationValidationSchema, this.updateOrganizationValidationSchema,
this.validationResult, this.validationResult,
this.asyncMiddleware(this.updateOrganization.bind(this)), this.asyncMiddleware(this.updateOrganization.bind(this)),
this.handleServiceErrors.bind(this) this.handleServiceErrors.bind(this)
@@ -55,7 +55,7 @@ export default class OrganizationController extends BaseController {
* Organization setup schema. * Organization setup schema.
* @return {ValidationChain[]} * @return {ValidationChain[]}
*/ */
private get organizationValidationSchema(): ValidationChain[] { private get commonOrganizationValidationSchema(): ValidationChain[] {
return [ return [
check('name').exists().trim(), check('name').exists().trim(),
check('industry').optional({ nullable: true }).isString().trim().escape(), check('industry').optional({ nullable: true }).isString().trim().escape(),
@@ -68,6 +68,29 @@ export default class OrganizationController extends BaseController {
]; ];
} }
/**
* Build organization validation schema.
* @returns {ValidationChain[]}
*/
private get buildOrganizationValidationSchema(): ValidationChain[] {
return [...this.commonOrganizationValidationSchema];
}
/**
* Update organization validation schema.
* @returns {ValidationChain[]}
*/
private get updateOrganizationValidationSchema(): ValidationChain[] {
return [
...this.commonOrganizationValidationSchema,
check('tax_number')
.optional({ nullable: true })
.isString()
.trim()
.escape(),
];
}
/** /**
* Builds tenant database and migrate database schema. * Builds tenant database and migrate database schema.
* @param {Request} req - Express request. * @param {Request} req - Express request.

View File

@@ -16,6 +16,8 @@ export interface ILedger {
getClosingBalance(): number; getClosingBalance(): number;
getForeignClosingBalance(): number; getForeignClosingBalance(): number;
getClosingDebit(): number;
getClosingCredit(): number;
getContactsIds(): number[]; getContactsIds(): number[];
getAccountsIds(): number[]; getAccountsIds(): number[];

View File

@@ -25,6 +25,7 @@ export interface IOrganizationUpdateDTO {
timezone: string; timezone: string;
fiscalYear: string; fiscalYear: string;
industry: string; industry: string;
taxNumber: string;
} }
export interface IOrganizationBuildEventPayload { export interface IOrganizationBuildEventPayload {

View File

@@ -33,6 +33,7 @@ export interface ITrialBalanceAccount extends ITrialBalanceTotal {
id: number; id: number;
parentAccountId: number; parentAccountId: number;
name: string; name: string;
formattedName: string;
code: string; code: string;
accountNormal: string; accountNormal: string;
} }

View File

@@ -1,5 +1,5 @@
import moment from 'moment'; import moment from 'moment';
import { defaultTo, uniqBy } from 'lodash'; import { defaultTo, sumBy, uniqBy } from 'lodash';
import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces'; import { IAccountTransaction, ILedger, ILedgerEntry } from '@/interfaces';
export default class Ledger implements ILedger { export default class Ledger implements ILedger {
@@ -49,6 +49,15 @@ export default class Ledger implements ILedger {
return this.filter((entry) => entry.accountId === accountId); return this.filter((entry) => entry.accountId === accountId);
} }
/**
* Filters entries by the given accounts ids then returns a new ledger.
* @param {number[]} accountsIds - Accounts ids.
* @returns {ILedger}
*/
public whereAccountsIds(accountsIds: number[]): ILedger {
return this.filter((entry) => accountsIds.indexOf(entry.accountId) !== -1);
}
/** /**
* Filters entries that before or same the given date and returns a new ledger. * Filters entries that before or same the given date and returns a new ledger.
* @param {Date|string} fromDate * @param {Date|string} fromDate
@@ -130,6 +139,22 @@ export default class Ledger implements ILedger {
return closingBalance; return closingBalance;
} }
/**
* Retrieves the closing credit of the entries.
* @returns {number}
*/
public getClosingCredit(): number {
return sumBy(this.entries, 'credit');
}
/**
* Retrieves the closing debit of the entries.
* @returns {number}
*/
public getClosingDebit(): number {
return sumBy(this.entries, 'debit');
}
/** /**
* Retrieve the closing balance of the entries. * Retrieve the closing balance of the entries.
* @returns {number} * @returns {number}

View File

@@ -10,13 +10,26 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import { allPassedConditionsPass, flatToNestedArray } from 'utils'; import { allPassedConditionsPass, flatToNestedArray } from 'utils';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
export default class TrialBalanceSheet extends FinancialSheet { export default class TrialBalanceSheet extends FinancialSheet {
tenantId: number; /**
query: ITrialBalanceSheetQuery; * Trial balance sheet query.
accounts: IAccount & { type: IAccountType }[]; * @param {ITrialBalanceSheetQuery} query
journalFinancial: any; */
baseCurrency: string; private query: ITrialBalanceSheetQuery;
/**
* Trial balance sheet repository.
* @param {TrialBalanceSheetRepository}
*/
private repository: TrialBalanceSheetRepository;
/**
* Organization base currency.
* @param {string}
*/
private baseCurrency: string;
/** /**
* Constructor method. * Constructor method.
@@ -28,20 +41,58 @@ export default class TrialBalanceSheet extends FinancialSheet {
constructor( constructor(
tenantId: number, tenantId: number,
query: ITrialBalanceSheetQuery, query: ITrialBalanceSheetQuery,
accounts: IAccount & { type: IAccountType }[], repository: TrialBalanceSheetRepository,
journalFinancial: any,
baseCurrency: string baseCurrency: string
) { ) {
super(); super();
this.tenantId = tenantId; this.tenantId = tenantId;
this.query = query; this.query = query;
this.accounts = accounts; this.repository = repository;
this.journalFinancial = journalFinancial;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.baseCurrency = baseCurrency; this.baseCurrency = baseCurrency;
} }
/**
* Retrieves the closing credit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountCredit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingCredit();
}
/**
* Retrieves the closing debit of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountDebit(accountId: number) {
const depsAccountsIds =
this.repository.accountsDepGraph.dependenciesOf(accountId);
return this.repository.totalAccountsLedger
.whereAccountsIds([accountId, ...depsAccountsIds])
.getClosingDebit();
}
/**
* Retrieves the closing total of the given account.
* @param {number} accountId
* @returns {number}
*/
public getClosingAccountTotal(accountId: number) {
const credit = this.getClosingAccountCredit(accountId);
const debit = this.getClosingAccountDebit(accountId);
return debit - credit;
}
/** /**
* Account mapper. * Account mapper.
* @param {IAccount} account * @param {IAccount} account
@@ -50,23 +101,28 @@ export default class TrialBalanceSheet extends FinancialSheet {
private accountTransformer = ( private accountTransformer = (
account: IAccount & { type: IAccountType } account: IAccount & { type: IAccountType }
): ITrialBalanceAccount => { ): ITrialBalanceAccount => {
const trial = this.journalFinancial.getTrialBalanceWithDepands(account.id); const debit = this.getClosingAccountDebit(account.id);
const credit = this.getClosingAccountCredit(account.id);
const balance = this.getClosingAccountTotal(account.id);
return { return {
id: account.id, id: account.id,
parentAccountId: account.parentAccountId, parentAccountId: account.parentAccountId,
name: account.name, name: account.name,
formattedName: account.code
? `${account.name} - ${account.code}`
: `${account.name}`,
code: account.code, code: account.code,
accountNormal: account.accountNormal, accountNormal: account.accountNormal,
credit: trial.credit, credit,
debit: trial.debit, debit,
balance: trial.balance, balance,
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
formattedCredit: this.formatNumber(trial.credit), formattedCredit: this.formatNumber(credit),
formattedDebit: this.formatNumber(trial.debit), formattedDebit: this.formatNumber(debit),
formattedBalance: this.formatNumber(trial.balance), formattedBalance: this.formatNumber(balance),
}; };
}; };
@@ -117,10 +173,7 @@ export default class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = ( private filterNoneTransactions = (
accountNode: ITrialBalanceAccount accountNode: ITrialBalanceAccount
): boolean => { ): boolean => {
const entries = this.journalFinancial.getAccountEntriesWithDepents( return false === this.repository.totalAccountsLedger.isEmpty();
accountNode.id
);
return entries.length > 0;
}; };
/** /**
@@ -200,11 +253,11 @@ export default class TrialBalanceSheet extends FinancialSheet {
*/ */
public reportData(): ITrialBalanceSheetData { public reportData(): ITrialBalanceSheetData {
// Don't return noting if the journal has no transactions. // Don't return noting if the journal has no transactions.
if (this.journalFinancial.isEmpty()) { if (this.repository.totalAccountsLedger.isEmpty()) {
return null; return null;
} }
// Retrieve accounts nodes. // Retrieve accounts nodes.
const accounts = this.accountsSection(this.accounts); const accounts = this.accountsSection(this.repository.accounts);
// Retrieve account node. // Retrieve account node.
const total = this.tatalSection(accounts); const total = this.tatalSection(accounts);

View File

@@ -0,0 +1,105 @@
import { ITrialBalanceSheetQuery } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { Knex } from 'knex';
import { isEmpty } from 'lodash';
import { Service } from 'typedi';
@Service()
export class TrialBalanceSheetRepository {
private query: ITrialBalanceSheetQuery;
private models: any;
public accounts: any;
public accountsDepGraph;
/**
* Total closing accounts ledger.
* @param {Ledger}
*/
public totalAccountsLedger: Ledger;
/**
* Constructor method.
* @param {number} tenantId
* @param {IBalanceSheetQuery} query
*/
constructor(models: any, repos: any, query: ITrialBalanceSheetQuery) {
this.query = query;
this.repos = repos;
this.models = models;
}
/**
* Async initialize.
* @returns {Promise<void>}
*/
public asyncInitialize = async () => {
await this.initAccounts();
await this.initAccountsClosingTotalLedger();
};
// ----------------------------
// # Accounts
// ----------------------------
/**
* Initialize accounts.
* @returns {Promise<void>}
*/
public initAccounts = async () => {
const accounts = await this.getAccounts();
const accountsDepGraph =
await this.repos.accountRepository.getDependencyGraph();
this.accountsDepGraph = accountsDepGraph;
this.accounts = accounts;
};
/**
* Initialize all accounts closing total ledger.
* @return {Promise<void>}
*/
public initAccountsClosingTotalLedger = async (): Promise<void> => {
const totalByAccounts = await this.closingAccountsTotal(this.query.toDate);
this.totalAccountsLedger = Ledger.fromTransactions(totalByAccounts);
};
/**
* Retrieve accounts of the report.
* @return {Promise<IAccount[]>}
*/
private getAccounts = () => {
const { Account } = this.models;
return Account.query();
};
/**
* Retrieve the opening balance transactions of the report.
* @param {Date|string} openingDate -
*/
public closingAccountsTotal = async (openingDate: Date | string) => {
const { AccountTransaction } = this.models;
return AccountTransaction.query().onBuild((query) => {
query.sum('credit as credit');
query.sum('debit as debit');
query.groupBy('accountId');
query.select(['accountId']);
query.modify('filterDateRange', null, openingDate);
query.withGraphFetched('account');
this.commonFilterBranchesQuery(query);
});
};
/**
* Common branches filter query.
* @param {Knex.QueryBuilder} query
*/
private commonFilterBranchesQuery = (query: Knex.QueryBuilder) => {
if (!isEmpty(this.query.branchesIds)) {
query.modify('filterByBranches', this.query.branchesIds);
}
};
}

View File

@@ -2,12 +2,18 @@ import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster'; import Journal from '@/services/Accounting/JournalPoster';
import { ITrialBalanceSheetMeta, ITrialBalanceSheetQuery, ITrialBalanceStatement } from '@/interfaces'; import {
ITrialBalanceSheetMeta,
ITrialBalanceSheetQuery,
ITrialBalanceStatement,
} from '@/interfaces';
import TrialBalanceSheet from './TrialBalanceSheet'; import TrialBalanceSheet from './TrialBalanceSheet';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import InventoryService from '@/services/Inventory/Inventory'; import InventoryService from '@/services/Inventory/Inventory';
import { parseBoolean } from 'utils'; import { parseBoolean } from 'utils';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import { TrialBalanceSheetRepository } from './TrialBalanceSheetRepository';
import { TrialBalanceSheetTable } from './TrialBalanceSheetTable';
@Service() @Service()
export default class TrialBalanceSheetService extends FinancialSheet { export default class TrialBalanceSheetService extends FinancialSheet {
@@ -51,9 +57,8 @@ export default class TrialBalanceSheetService extends FinancialSheet {
reportMetadata(tenantId: number): ITrialBalanceSheetMeta { reportMetadata(tenantId: number): ITrialBalanceSheetMeta {
const settings = this.tenancy.settings(tenantId); const settings = this.tenancy.settings(tenantId);
const isCostComputeRunning = this.inventoryService.isItemsCostComputeRunning( const isCostComputeRunning =
tenantId this.inventoryService.isItemsCostComputeRunning(tenantId);
);
const organizationName = settings.get({ const organizationName = settings.get({
group: 'organization', group: 'organization',
key: 'name', key: 'name',
@@ -72,10 +77,8 @@ export default class TrialBalanceSheetService extends FinancialSheet {
/** /**
* Retrieve trial balance sheet statement. * Retrieve trial balance sheet statement.
* -------------
* @param {number} tenantId * @param {number} tenantId
* @param {IBalanceSheetQuery} query * @param {IBalanceSheetQuery} query
*
* @return {IBalanceSheetStatement} * @return {IBalanceSheetStatement}
*/ */
public async trialBalanceSheet( public async trialBalanceSheet(
@@ -86,43 +89,27 @@ export default class TrialBalanceSheetService extends FinancialSheet {
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
const {
accountRepository,
transactionsRepository,
} = this.tenancy.repositories(tenantId);
const tenant = await Tenant.query() const tenant = await Tenant.query()
.findById(tenantId) .findById(tenantId)
.withGraphFetched('metadata'); .withGraphFetched('metadata');
this.logger.info('[trial_balance_sheet] trying to calcualte the report.', { const models = this.tenancy.models(tenantId);
tenantId, const repos = this.tenancy.repositories(tenantId);
filter,
});
// Retrieve all accounts on the storage.
const accounts = await accountRepository.all();
const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieve all journal transactions based on the given query. const trialBalanceSheetRepos = new TrialBalanceSheetRepository(
const transactions = await transactionsRepository.journal({ models,
fromDate: query.fromDate, repos,
toDate: query.toDate, filter
sumationCreditDebit: true,
branchesIds: query.branchesIds
});
// Transform transactions array to journal collection.
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
); );
await trialBalanceSheetRepos.asyncInitialize();
// Trial balance report instance. // Trial balance report instance.
const trialBalanceInstance = new TrialBalanceSheet( const trialBalanceInstance = new TrialBalanceSheet(
tenantId, tenantId,
filter, filter,
accounts, trialBalanceSheetRepos,
transactionsJournal, tenant.metadata.baseCurrency
tenant.metadata.baseCurrency,
); );
// Trial balance sheet data. // Trial balance sheet data.
const trialBalanceSheetData = trialBalanceInstance.reportData(); const trialBalanceSheetData = trialBalanceInstance.reportData();
@@ -133,4 +120,27 @@ export default class TrialBalanceSheetService extends FinancialSheet {
meta: this.reportMetadata(tenantId), meta: this.reportMetadata(tenantId),
}; };
} }
/**
* Retrieves the trial balance sheet table.
* @param {number} tenantId
* @param {ITrialBalanceSheetQuery} query
* @returns {Promise<any>}
*/
public async trialBalanceSheetTable(
tenantId: number,
query: ITrialBalanceSheetQuery
) {
const trialBalance = await this.trialBalanceSheet(tenantId, query);
const table = new TrialBalanceSheetTable(trialBalance.data, query, {});
return {
table: {
columns: table.tableColumns(),
rows: table.tableRows(),
},
meta: trialBalance.meta,
query: trialBalance.query,
};
}
} }

View File

@@ -0,0 +1,146 @@
import * as R from 'ramda';
import FinancialSheet from '../FinancialSheet';
import { FinancialTable } from '../FinancialTable';
import {
IBalanceSheetStatementData,
ITableColumn,
ITableColumnAccessor,
ITableRow,
ITrialBalanceAccount,
ITrialBalanceSheetData,
ITrialBalanceSheetQuery,
ITrialBalanceTotal,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import { IROW_TYPE } from '../BalanceSheet/constants';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
export class TrialBalanceSheetTable extends R.compose(
FinancialTable,
FinancialSheetStructure
)(FinancialSheet) {
/**
* @param {ITrialBalanceSheetData}
*/
public data: ITrialBalanceSheetData;
/**
* Balance sheet query.
* @param {ITrialBalanceSheetQuery}
*/
public query: ITrialBalanceSheetQuery;
/**
* Constructor method.
* @param {IBalanceSheetStatementData} reportData -
* @param {ITrialBalanceSheetQuery} query -
*/
constructor(
data: ITrialBalanceSheetData,
query: ITrialBalanceSheetQuery,
i18n: any
) {
super();
this.data = data;
this.query = query;
this.i18n = i18n;
}
/**
* Retrieve the common columns for all report nodes.
* @param {ITableColumnAccessor[]}
*/
private commonColumnsAccessors = (): ITableColumnAccessor[] => {
return [
{ key: 'account', accessor: 'formattedName' },
{ key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'total', accessor: 'formattedBalance' },
];
};
/**
* Maps the account node to table row.
* @param {ITrialBalanceAccount} node -
* @returns {ITableRow}
*/
private accountNodeTableRowsMapper = (
node: ITrialBalanceAccount
): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.ACCOUNT],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Maps the total node to table row.
* @param {ITrialBalanceTotal} node -
* @returns {ITableRow}
*/
private totalNodeTableRowsMapper = (node: ITrialBalanceTotal): ITableRow => {
const columns = this.commonColumnsAccessors();
const meta = {
rowTypes: [IROW_TYPE.TOTAL],
id: node.id,
};
return tableRowMapper(node, columns, meta);
};
/**
* Mappes the given report sections to table rows.
* @param {IBalanceSheetDataNode[]} nodes -
* @return {ITableRow}
*/
private accountsToTableRowsMap = (
nodes: ITrialBalanceAccount[]
): ITableRow[] => {
return this.mapNodesDeep(nodes, this.accountNodeTableRowsMapper);
};
/**
* Retrieves the accounts table rows of the given report data.
* @returns {ITableRow[]}
*/
private accountsTableRows = (): ITableRow[] => {
return this.accountsToTableRowsMap(this.data.accounts);
};
/**
* Maps the given total node to table row.
* @returns {ITableRow}
*/
private totalTableRow = (): ITableRow => {
return this.totalNodeTableRowsMapper(this.data.total);
};
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.append(this.totalTableRow()),
R.concat(this.accountsTableRows())
)([]);
};
/**
* Retrrieves the table columns.
* @returns {ITableColumn[]}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(
this.tableColumnsCellIndexing,
R.concat([
{ key: 'account_name', label: 'Account' },
{ key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
{ key: 'total', label: 'Total' },
])
)([]);
};
}

View File

@@ -0,0 +1,5 @@
export enum IROW_TYPE {
ACCOUNT = 'ACCOUNT',
TOTAL = 'TOTAL',
}

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.string('tax_number')
});
};
exports.down = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.dropColumn('tax_number');
});
};

View File

@@ -62,6 +62,7 @@ const BalanceSheetDataTable = styled(ReportDataTable)`
border-bottom: 0; border-bottom: 0;
padding-top: 0.32rem; padding-top: 0.32rem;
padding-bottom: 0.32rem; padding-bottom: 0.32rem;
color: #252A31;
} }
&.is-expanded { &.is-expanded {
.td:not(.name) .cell-inner { .td:not(.name) .cell-inner {
@@ -72,6 +73,7 @@ const BalanceSheetDataTable = styled(ReportDataTable)`
.td { .td {
font-weight: 500; font-weight: 500;
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
color: #000;
} }
} }

View File

@@ -60,9 +60,8 @@ const CashflowStatementDataTable = styled(DataTable)`
border-bottom: 0; border-bottom: 0;
padding-top: 0.32rem; padding-top: 0.32rem;
padding-bottom: 0.32rem; padding-bottom: 0.32rem;
color: #252a31;
} }
// &.row-type--AGGREGATE,
&.row_type--ACCOUNTS { &.row_type--ACCOUNTS {
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
} }
@@ -72,6 +71,9 @@ const CashflowStatementDataTable = styled(DataTable)`
&.row_type--TOTAL { &.row_type--TOTAL {
font-weight: 500; font-weight: 500;
.td {
color: #000;
}
&:not(:first-child) .td { &:not(:first-child) .td {
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
} }

View File

@@ -61,6 +61,7 @@ const ProfitLossDataTable = styled(ReportDataTable)`
border-bottom: 0; border-bottom: 0;
padding-top: 0.32rem; padding-top: 0.32rem;
padding-bottom: 0.32rem; padding-bottom: 0.32rem;
color: #252A31;
} }
&.is-expanded { &.is-expanded {
.td:not(.name) .cell-inner { .td:not(.name) .cell-inner {
@@ -71,6 +72,7 @@ const ProfitLossDataTable = styled(ReportDataTable)`
.td { .td {
font-weight: 500; font-weight: 500;
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
color: #000;
} }
} }
&:last-of-type .td { &:last-of-type .td {

View File

@@ -15,7 +15,6 @@ import {
} from './components'; } from './components';
import withTrialBalanceActions from './withTrialBalanceActions'; import withTrialBalanceActions from './withTrialBalanceActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
/** /**

View File

@@ -8,8 +8,7 @@ import { tableRowTypesToClassnames } from '@/utils';
import { ReportDataTable, FinancialSheet } from '@/components'; import { ReportDataTable, FinancialSheet } from '@/components';
import { useTrialBalanceSheetContext } from './TrialBalanceProvider'; import { useTrialBalanceSheetContext } from './TrialBalanceProvider';
import { useTrialBalanceTableColumns } from './components'; import { useTrialBalanceSheetTableColumns } from './hooks';
/** /**
* Trial Balance sheet data table. * Trial Balance sheet data table.
@@ -17,12 +16,12 @@ import { useTrialBalanceTableColumns } from './components';
export default function TrialBalanceSheetTable({ companyName }) { export default function TrialBalanceSheetTable({ companyName }) {
// Trial balance sheet context. // Trial balance sheet context.
const { const {
trialBalanceSheet: { tableRows, query }, trialBalanceSheet: { table, query },
isLoading, isLoading,
} = useTrialBalanceSheetContext(); } = useTrialBalanceSheetContext();
// Trial balance sheet table columns. // Trial balance sheet table columns.
const columns = useTrialBalanceTableColumns(); const columns = useTrialBalanceSheetTableColumns();
return ( return (
<FinancialSheet <FinancialSheet
@@ -36,7 +35,7 @@ export default function TrialBalanceSheetTable({ companyName }) {
> >
<TrialBalanceDataTable <TrialBalanceDataTable
columns={columns} columns={columns}
data={tableRows} data={table.rows}
expandable={true} expandable={true}
expandToggleColumn={1} expandToggleColumn={1}
expandColumnSpace={1} expandColumnSpace={1}
@@ -59,7 +58,7 @@ const TrialBalanceDataTable = styled(ReportDataTable)`
.balance.td { .balance.td {
border-top-color: #000; border-top-color: #000;
} }
.tr.row_type--total .td { .tr.row_type--TOTAL .td {
border-top: 1px solid #bbb; border-top: 1px solid #bbb;
font-weight: 500; font-weight: 500;
border-bottom: 3px double #000; border-bottom: 3px double #000;

View File

@@ -1,88 +1,10 @@
// @ts-nocheck // @ts-nocheck
import React from 'react';
import intl from 'react-intl-universal';
import { Button } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
import { CellTextSpan } from '@/components/Datatable/Cells';
import { If, Icon, FormattedMessage as T } from '@/components'; import { If, Icon, FormattedMessage as T } from '@/components';
import { useTrialBalanceSheetContext } from './TrialBalanceProvider'; import { useTrialBalanceSheetContext } from './TrialBalanceProvider';
import { FinancialComputeAlert } from '../FinancialReportPage'; import { FinancialComputeAlert } from '../FinancialReportPage';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
/**
* Retrieves the credit column.
*/
const getCreditColumn = (data) => {
const width = getColumnWidth(data, `credit`, { minWidth: 140 });
return {
Header: intl.get('credit'),
Cell: CellTextSpan,
accessor: 'formatted_credit',
className: 'credit',
width,
textOverview: true,
align: Align.Right,
};
};
/**
* Retrieves the debit column.
*/
const getDebitColumn = (data) => {
return {
Header: intl.get('debit'),
Cell: CellTextSpan,
accessor: 'formatted_debit',
width: getColumnWidth(data, `debit`, { minWidth: 140 }),
textOverview: true,
align: Align.Right,
};
};
/**
* Retrieves the balance column.
*/
const getBalanceColumn = (data) => {
return {
Header: intl.get('balance'),
Cell: CellTextSpan,
accessor: 'formatted_balance',
className: 'balance',
width: getColumnWidth(data, `balance`, { minWidth: 140 }),
textOverview: true,
align: Align.Right,
};
};
/**
* Retrieve trial balance sheet table columns.
*/
export const useTrialBalanceTableColumns = () => {
// Trial balance sheet context.
const {
trialBalanceSheet: { tableRows },
} = useTrialBalanceSheetContext();
return React.useMemo(
() => [
{
Header: intl.get('account_name'),
accessor: (row) => (row.code ? `${row.name} - ${row.code}` : row.name),
className: 'name',
width: 350,
textOverview: true,
},
getCreditColumn(tableRows),
getDebitColumn(tableRows),
getBalanceColumn(tableRows),
],
[tableRows],
);
};
/** /**
* Trial balance sheet progress loading bar. * Trial balance sheet progress loading bar.
*/ */
@@ -101,7 +23,7 @@ export function TrialBalanceSheetLoadingBar() {
*/ */
export function TrialBalanceSheetAlerts() { export function TrialBalanceSheetAlerts() {
const { const {
trialBalanceSheet: { meta }, trialBalanceSheet,
isLoading, isLoading,
refetchSheet, refetchSheet,
} = useTrialBalanceSheetContext(); } = useTrialBalanceSheetContext();
@@ -115,7 +37,7 @@ export function TrialBalanceSheetAlerts() {
return null; return null;
} }
// Can't continue if the cost compute job is not running. // Can't continue if the cost compute job is not running.
if (!meta.is_cost_compute_running) { if (!trialBalanceSheet?.meta.is_cost_compute_running) {
return null; return null;
} }

View File

@@ -0,0 +1,56 @@
// @ts-nocheck
import * as R from 'ramda';
import { Align } from '@/constants';
import { getColumnWidth } from '@/utils';
const ACCOUNT_NAME_COLUMN_WIDTH = 320;
const AMOUNT_COLUMNS_MIN_WIDTH = 120;
const AMOUNT_COLUMNS_MAGIC_SPACING = 10;
const getTableCellValueAccessor = (index: number) => `cells[${index}].value`;
const accountNameAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
Header: column.label,
id: column.key,
accessor,
className: column.key,
width: ACCOUNT_NAME_COLUMN_WIDTH,
};
});
const amountAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
Header: column.label,
id: column.key,
accessor,
className: column.key,
width: getColumnWidth(data, accessor, {
magicSpacing: AMOUNT_COLUMNS_MAGIC_SPACING,
minWidth: AMOUNT_COLUMNS_MIN_WIDTH,
}),
align: Align.Right,
};
});
const dynamicColumnMapper = R.curry((data, column) => {
const accountNameColumn = accountNameAccessor(data);
const creditColumn = amountAccessor(data);
const debitColumn = amountAccessor(data);
const totalColumn = amountAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'account_name'), accountNameColumn),
R.when(R.pathEq(['key'], 'credit'), creditColumn),
R.when(R.pathEq(['key'], 'debit'), debitColumn),
R.when(R.pathEq(['key'], 'total'), totalColumn),
)(column);
});
export const trialBalancesheetDynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import React from 'react';
import { useTrialBalanceSheetContext } from './TrialBalanceProvider';
import { trialBalancesheetDynamicColumns } from './dynamicColumns';
/**
* Retrieves the trial balance sheet columns.
*/
export const useTrialBalanceSheetTableColumns = () => {
const {
trialBalanceSheet: { table },
} = useTrialBalanceSheetContext();
return React.useMemo(
() => trialBalancesheetDynamicColumns(table.columns, table.rows),
[table],
);
};

View File

@@ -6,6 +6,9 @@ const Schema = Yup.object().shape({
name: Yup.string() name: Yup.string()
.required() .required()
.label(intl.get('organization_name_')), .label(intl.get('organization_name_')),
tax_number: Yup.string()
.nullable()
.label(intl.get('organization_tax_number_')),
industry: Yup.string() industry: Yup.string()
.nullable() .nullable()
.label(intl.get('organization_industry_')), .label(intl.get('organization_industry_')),

View File

@@ -59,6 +59,17 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
<FInputGroup medium={'true'} name={'name'} fastField={true} /> <FInputGroup medium={'true'} name={'name'} fastField={true} />
</FFormGroup> </FFormGroup>
{/* ---------- Organization Tax Number ---------- */}
<FFormGroup
name={'tax_number'}
label={<T id={'organization_tax_number'} />}
inline={true}
helperText={<T id={'shown_on_sales_forms_and_purchase_orders'} />}
fastField={true}
>
<FInputGroup medium={'true'} name={'tax_number'} fastField={true} />
</FFormGroup>
{/* ---------- Industry ---------- */} {/* ---------- Industry ---------- */}
<FFormGroup <FFormGroup
name={'industry'} name={'industry'}

View File

@@ -23,6 +23,7 @@ const defaultValues = {
fiscal_year: '', fiscal_year: '',
date_format: '', date_format: '',
timezone: '', timezone: '',
tax_number: '',
}; };
/** /**

View File

@@ -43,17 +43,12 @@ export function useTrialBalanceSheet(query, props) {
method: 'get', method: 'get',
url: '/financial_statements/trial_balance_sheet', url: '/financial_statements/trial_balance_sheet',
params: query, params: query,
headers: {
Accept: 'application/json+table',
},
}, },
{ {
select: (res) => ({ select: (res) => res.data,
tableRows: trialBalanceSheetReducer(res.data.data),
...res.data,
}),
defaultData: {
tableRows: [],
data: [],
query: {},
},
...props, ...props,
}, },
); );

View File

@@ -37,6 +37,7 @@
"success": "Success", "success": "Success",
"register_a_new_organization": "Register a New Organization.", "register_a_new_organization": "Register a New Organization.",
"organization_name": "Organization Name", "organization_name": "Organization Name",
"organization_tax_number": "Organization Tax Number",
"email": "Email", "email": "Email",
"email_address": "Email Address", "email_address": "Email Address",
"register": "Register", "register": "Register",
@@ -339,6 +340,7 @@
"item_type_": "Item type", "item_type_": "Item type",
"item_name_": "Item name", "item_name_": "Item name",
"organization_industry_": "Organization industry", "organization_industry_": "Organization industry",
"organization_tax_number_": "Organization tax number",
"base_currency_": "Base currency", "base_currency_": "Base currency",
"date_format_": "Date format", "date_format_": "Date format",
"category_name_": "Category name", "category_name_": "Category name",
@@ -1804,6 +1806,7 @@
"balance_sheet.total_change": "Total Change", "balance_sheet.total_change": "Total Change",
"balance_sheet.change": "% Change", "balance_sheet.change": "% Change",
"balance_sheet.previous_period": "Previous Period (PP)", "balance_sheet.previous_period": "Previous Period (PP)",
"balance_sheet.net_income": "Net Income",
"profit_loss_sheet.comparisons": "Comparisons", "profit_loss_sheet.comparisons": "Comparisons",
"profit_loss_sheet.dimensions": "Dimensions", "profit_loss_sheet.dimensions": "Dimensions",
"profit_loss_sheet.previous_year": "Previous Year", "profit_loss_sheet.previous_year": "Previous Year",