Merge pull request #229 from bigcapitalhq/abouhuolia/big-45-receivablepayable-again-report-issue

fix AP/AR aging summary issue
This commit is contained in:
Ahmed Bouhuolia
2023-08-27 00:58:17 +02:00
committed by GitHub
24 changed files with 599 additions and 235 deletions

View File

@@ -33,10 +33,13 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('as_date').optional().isISO8601(), query('as_date').optional().isISO8601(),
query('aging_days_before').optional().isNumeric().toInt(),
query('aging_periods').optional().isNumeric().toInt(), query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
query('vendors_ids').optional().isArray({ min: 1 }), query('vendors_ids').optional().isArray({ min: 1 }),
query('vendors_ids.*').isInt({ min: 1 }).toInt(), query('vendors_ids.*').isInt({ min: 1 }).toInt(),
query('none_zero').default(true).isBoolean().toBoolean(), query('none_zero').default(true).isBoolean().toBoolean(),
// Filtering by branches. // Filtering by branches.
@@ -53,15 +56,36 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { try {
const { data, columns, query, meta } = const accept = this.accepts(req);
await this.APAgingSummaryService.APAgingSummary(tenantId, filter); const acceptType = accept.types(['json', 'application/json+table']);
return res.status(200).send({ switch (acceptType) {
data: this.transfromToResponse(data), case 'application/json+table':
columns: this.transfromToResponse(columns), const table = await this.APAgingSummaryService.APAgingSummaryTable(
query: this.transfromToResponse(query), tenantId,
meta: this.transfromToResponse(meta), filter
}); );
return res.status(200).send({
table: {
rows: table.rows,
columns: table.columns,
},
meta: table.meta,
query: table.query,
});
break;
default:
const { data, columns, query, meta } =
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
return res.status(200).send({
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
break;
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -36,8 +36,8 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
query('as_date').optional().isISO8601(), query('as_date').optional().isISO8601(),
query('aging_days_before').optional().isInt({ max: 500 }).toInt(), query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
query('aging_periods').optional().isInt({ max: 12 }).toInt(), query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
query('customers_ids').optional().isArray({ min: 1 }), query('customers_ids').optional().isArray({ min: 1 }),
query('customers_ids.*').isInt({ min: 1 }).toInt(), query('customers_ids.*').isInt({ min: 1 }).toInt(),
@@ -58,15 +58,36 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { try {
const { data, columns, query, meta } = const accept = this.accepts(req);
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter); const acceptType = accept.types(['json', 'application/json+table']);
return res.status(200).send({ switch (acceptType) {
data: this.transfromToResponse(data), case 'application/json+table':
columns: this.transfromToResponse(columns), const table = await this.ARAgingSummaryService.ARAgingSummaryTable(
query: this.transfromToResponse(query), tenantId,
meta: this.transfromToResponse(meta), filter
}); );
return res.status(200).send({
table: {
rows: table.rows,
columns: table.columns,
},
meta: table.meta,
query: table.query,
});
break;
default:
const { data, columns, query, meta } =
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
return res.status(200).send({
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
});
break;
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -1,51 +1,36 @@
import { import {
IAgingPeriod, IAgingPeriod,
IAgingPeriodTotal, IAgingPeriodTotal,
IAgingAmount IAgingAmount,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from './AgingReport'; } from './AgingReport';
import { import { INumberFormatQuery } from './FinancialStatements';
INumberFormatQuery
} from './FinancialStatements';
export interface IAPAgingSummaryQuery { export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
asDate: Date | string;
agingDaysBefore: number;
agingPeriods: number;
numberFormat: INumberFormatQuery;
vendorsIds: number[]; vendorsIds: number[];
noneZero: boolean;
branchesIds?: number[]
} }
export interface IAPAgingSummaryVendor { export interface IAPAgingSummaryVendor extends IAgingSummaryContact {
vendorName: string, vendorName: string;
current: IAgingAmount, }
aging: IAgingPeriodTotal[],
total: IAgingAmount,
};
export interface IAPAgingSummaryTotal { export interface IAPAgingSummaryTotal extends IAgingSummaryTotal {}
current: IAgingAmount,
aging: IAgingPeriodTotal[],
total: IAgingAmount,
};
export interface IAPAgingSummaryData { export interface IAPAgingSummaryData extends IAgingSummaryData {
vendors: IAPAgingSummaryVendor[], vendors: IAPAgingSummaryVendor[];
total: IAPAgingSummaryTotal, }
};
export type IAPAgingSummaryColumns = IAgingPeriod[]; export type IAPAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta { export interface IARAgingSummaryMeta {
baseCurrency: string, baseCurrency: string;
organizationName: string, organizationName: string;
} }
export interface IAPAgingSummaryMeta { export interface IAPAgingSummaryMeta {
baseCurrency: string, baseCurrency: string;
organizationName: string, organizationName: string;
} }

View File

@@ -1,37 +1,28 @@
import { IAgingPeriod, IAgingPeriodTotal, IAgingAmount } from './AgingReport'; import {
import { INumberFormatQuery } from './FinancialStatements'; IAgingPeriod,
IAgingSummaryQuery,
IAgingSummaryTotal,
IAgingSummaryContact,
IAgingSummaryData,
} from './AgingReport';
export interface IARAgingSummaryQuery { export interface IARAgingSummaryQuery extends IAgingSummaryQuery {
asDate: Date | string;
agingDaysBefore: number;
agingPeriods: number;
numberFormat: INumberFormatQuery;
customersIds: number[]; customersIds: number[];
branchesIds: number[];
noneZero: boolean;
} }
export interface IARAgingSummaryCustomer { export interface IARAgingSummaryCustomer extends IAgingSummaryContact {
customerName: string; customerName: string;
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
} }
export interface IARAgingSummaryTotal { export interface IARAgingSummaryTotal extends IAgingSummaryTotal {}
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IARAgingSummaryData { export interface IARAgingSummaryData extends IAgingSummaryData {
customers: IARAgingSummaryCustomer[]; customers: IARAgingSummaryCustomer[];
total: IARAgingSummaryTotal;
} }
export type IARAgingSummaryColumns = IAgingPeriod[]; export type IARAgingSummaryColumns = IAgingPeriod[];
export interface IARAgingSummaryMeta { export interface IARAgingSummaryMeta {
organizationName: string, organizationName: string;
baseCurrency: string, baseCurrency: string;
} }

View File

@@ -1,6 +1,9 @@
import { INumberFormatQuery } from './FinancialStatements';
export interface IAgingPeriodTotal extends IAgingPeriod { export interface IAgingPeriodTotal extends IAgingPeriod {
total: IAgingAmount; total: IAgingAmount;
}; }
export interface IAgingAmount { export interface IAgingAmount {
amount: number; amount: number;
@@ -20,3 +23,22 @@ export interface IAgingSummaryContact {
aging: IAgingPeriodTotal[]; aging: IAgingPeriodTotal[];
total: IAgingAmount; total: IAgingAmount;
} }
export interface IAgingSummaryQuery {
asDate: Date | string;
agingDaysBefore: number;
agingPeriods: number;
numberFormat: INumberFormatQuery;
branchesIds: number[];
noneZero: boolean;
}
export interface IAgingSummaryTotal {
current: IAgingAmount;
aging: IAgingPeriodTotal[];
total: IAgingAmount;
}
export interface IAgingSummaryData {
total: IAgingSummaryTotal;
}

View File

@@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
import APAgingSummarySheet from './APAgingSummarySheet'; import APAgingSummarySheet from './APAgingSummarySheet';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import APAgingSummaryTable from './APAgingSummaryTable';
@Service() @Service()
export default class PayableAgingSummaryService { export default class PayableAgingSummaryService {
@@ -84,7 +85,7 @@ export default class PayableAgingSummaryService {
// Common query. // Common query.
const commonQuery = (query) => { const commonQuery = (query) => {
if (isEmpty(filter.branchesIds)) { if (!isEmpty(filter.branchesIds)) {
query.modify('filterByBranches', filter.branchesIds); query.modify('filterByBranches', filter.branchesIds);
} }
}; };
@@ -118,4 +119,21 @@ export default class PayableAgingSummaryService {
meta: this.reportMetadata(tenantId), meta: this.reportMetadata(tenantId),
}; };
} }
/**
* Retrieves A/P aging summary in table format.
* @param {number} tenantId
* @param {IAPAgingSummaryQuery} query
*/
async APAgingSummaryTable(tenantId: number, query: IAPAgingSummaryQuery) {
const report = await this.APAgingSummary(tenantId, query);
const table = new APAgingSummaryTable(report.data, query, {});
return {
columns: table.tableColumns(),
rows: table.tableRows(),
meta: report.meta,
query: report.query,
};
}
} }

View File

@@ -91,7 +91,7 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
return { return {
vendorName: vendor.displayName, vendorName: vendor.displayName,
current: this.formatTotalAmount(currentTotal), current: this.formatAmount(currentTotal),
aging: agingPeriods, aging: agingPeriods,
total: this.formatTotalAmount(amount), total: this.formatTotalAmount(amount),
}; };

View File

@@ -0,0 +1,46 @@
import {
IAPAgingSummaryData,
IAgingSummaryQuery,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import AgingSummaryTable from './AgingSummaryTable';
export default class APAgingSummaryTable extends AgingSummaryTable {
readonly report: IAPAgingSummaryData;
/**
* Constructor method.
* @param {IARAgingSummaryData} data
* @param {IAgingSummaryQuery} query
* @param {any} i18n
*/
constructor(data: IAPAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
super(data, query, i18n);
}
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
get contactsRows(): ITableRow[] {
return this.contactsNodes(this.report.vendors);
}
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'vendor_name', accessor: 'vendorName' };
}
/**
* Retrieves the contact name table column.
* @returns {ITableColumn}
*/
contactNameTableColumn = (): ITableColumn => {
return { label: 'Vendor name', key: 'vendor_name' };
};
}

View File

@@ -5,6 +5,7 @@ import { IARAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import ARAgingSummarySheet from './ARAgingSummarySheet'; import ARAgingSummarySheet from './ARAgingSummarySheet';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import ARAgingSummaryTable from './ARAgingSummaryTable';
@Service() @Service()
export default class ARAgingSummaryService { export default class ARAgingSummaryService {
@@ -89,12 +90,12 @@ export default class ARAgingSummaryService {
}; };
// Retrieve all overdue sale invoices. // Retrieve all overdue sale invoices.
const overdueSaleInvoices = await SaleInvoice.query() const overdueSaleInvoices = await SaleInvoice.query()
.modify('dueInvoicesFromDate', filter.asDate) .modify('overdueInvoicesFromDate', filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
// Retrieve all due sale invoices. // Retrieve all due sale invoices.
const currentInvoices = await SaleInvoice.query() const currentInvoices = await SaleInvoice.query()
.modify('overdueInvoicesFromDate', filter.asDate) .modify('dueInvoicesFromDate', filter.asDate)
.onBuild(commonQuery); .onBuild(commonQuery);
// AR aging summary report instance. // AR aging summary report instance.
@@ -117,4 +118,21 @@ export default class ARAgingSummaryService {
meta: this.reportMetadata(tenantId), meta: this.reportMetadata(tenantId),
}; };
} }
/**
* Retrieves A/R aging summary in table format.
* @param {number} tenantId
* @param {IARAgingSummaryQuery} query
*/
async ARAgingSummaryTable(tenantId: number, query: IARAgingSummaryQuery) {
const report = await this.ARAgingSummary(tenantId, query);
const table = new ARAgingSummaryTable(report.data, query, {});
return {
columns: table.tableColumns(),
rows: table.tableRows(),
meta: report.meta,
query,
};
}
} }

View File

@@ -1,4 +1,4 @@
import { groupBy, isEmpty, sum } from 'lodash'; import { Dictionary, groupBy, isEmpty, sum } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { import {
ICustomer, ICustomer,
@@ -54,7 +54,6 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
currentSaleInvoices, currentSaleInvoices,
'customerId' 'customerId'
); );
// Initializes the aging periods. // Initializes the aging periods.
this.agingPeriods = this.agingRangePeriods( this.agingPeriods = this.agingRangePeriods(
this.query.asDate, this.query.asDate,
@@ -189,7 +188,7 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
}; };
/** /**
* Retrieve AR aging summary report columns. * Retrieve A/R aging summary report columns.
* @return {IARAgingSummaryColumns} * @return {IARAgingSummaryColumns}
*/ */
public reportColumns(): IARAgingSummaryColumns { public reportColumns(): IARAgingSummaryColumns {

View File

@@ -0,0 +1,38 @@
import {
IARAgingSummaryData,
IAgingSummaryData,
IAgingSummaryQuery,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import AgingSummaryTable from './AgingSummaryTable';
export default class ARAgingSummaryTable extends AgingSummaryTable {
readonly report: IARAgingSummaryData;
/**
* Constructor method.
* @param {IARAgingSummaryData} data
* @param {IAgingSummaryQuery} query
* @param {any} i18n
*/
constructor(data: IARAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
super(data, query, i18n);
}
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
get contactsRows(): ITableRow[] {
return this.contactsNodes(this.report.customers);
}
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'customer_name', accessor: 'customerName' };
}
}

View File

@@ -0,0 +1,211 @@
import * as R from 'ramda';
import {
IAgingPeriod,
IAgingSummaryContact,
IAgingSummaryData,
IAgingSummaryQuery,
IAgingSummaryTotal,
ITableColumn,
ITableColumnAccessor,
ITableRow,
} from '@/interfaces';
import { tableRowMapper } from '@/utils';
import AgingReport from './AgingReport';
import { AgingSummaryRowType } from './_constants';
import { FinancialTable } from '../FinancialTable';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
export default abstract class AgingSummaryTable extends R.compose(
FinancialSheetStructure,
FinancialTable
)(AgingReport) {
protected readonly report: IAgingSummaryData;
protected readonly query: IAgingSummaryQuery;
protected readonly agingPeriods: IAgingPeriod[];
protected readonly i18n: any;
/**
* Constructor method.
* @param {IARAgingSummaryData} data
* @param {IAgingSummaryQuery} query
* @param {any} i18n
*/
constructor(data: IAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
super();
this.report = data;
this.i18n = i18n;
this.query = query;
this.agingPeriods = this.agingRangePeriods(
this.query.asDate,
this.query.agingDaysBefore,
this.query.agingPeriods
);
}
// -------------------------
// # Accessors.
// -------------------------
/**
* Aging accessors of contact and total nodes.
* @param {IAgingSummaryContact | IAgingSummaryTotal} node
* @returns {ITableColumnAccessor[]}
*/
protected agingNodeAccessors = (
node: IAgingSummaryContact | IAgingSummaryTotal
): ITableColumnAccessor[] => {
return node.aging.map((aging, index) => ({
key: 'aging',
accessor: `aging[${index}].total.formattedAmount`,
}));
};
/**
* Contact name node accessor.
* @returns {ITableColumnAccessor}
*/
protected get contactNameNodeAccessor(): ITableColumnAccessor {
return { key: 'customer_name', accessor: 'customerName' };
}
/**
* Retrieves the common columns for all report nodes.
* @param {IAgingSummaryContact}
* @returns {ITableColumnAccessor[]}
*/
protected contactNodeAccessors = (
node: IAgingSummaryContact
): ITableColumnAccessor[] => {
return R.compose(
R.concat([
this.contactNameNodeAccessor,
{ key: 'current', accessor: 'current.formattedAmount' },
...this.agingNodeAccessors(node),
{ key: 'total', accessor: 'total.formattedAmount' },
])
)([]);
};
/**
* Retrieves the contact name table row.
* @param {IAgingSummaryContact} node -
* @return {ITableRow}
*/
protected contactNameNode = (node: IAgingSummaryContact): ITableRow => {
const columns = this.contactNodeAccessors(node);
const meta = {
rowTypes: [AgingSummaryRowType.Contact],
};
return tableRowMapper(node, columns, meta);
};
/**
* Maps the customers nodes to table rows.
* @param {IAgingSummaryContact[]} nodes
* @returns {ITableRow[]}
*/
protected contactsNodes = (nodes: IAgingSummaryContact[]): ITableRow[] => {
return nodes.map(this.contactNameNode);
};
/**
* Retrieves the common columns for all report nodes.
* @param {IAgingSummaryTotal}
* @returns {ITableColumnAccessor[]}
*/
protected totalNodeAccessors = (
node: IAgingSummaryTotal
): ITableColumnAccessor[] => {
return R.compose(
R.concat([
{ key: 'blank', value: '' },
{ key: 'current', accessor: 'current.formattedAmount' },
...this.agingNodeAccessors(node),
{ key: 'total', accessor: 'total.formattedAmount' },
])
)([]);
};
/**
* Retrieves the total row of the given report total node.
* @param {IAgingSummaryTotal} node
* @returns {ITableRow}
*/
protected totalNode = (node: IAgingSummaryTotal): ITableRow => {
const columns = this.totalNodeAccessors(node);
const meta = {
rowTypes: [AgingSummaryRowType.Total],
};
return tableRowMapper(node, columns, meta);
};
// -------------------------
// # Computed Rows.
// -------------------------
/**
* Retrieves the contacts table rows.
* @returns {ITableRow[]}
*/
protected get contactsRows(): ITableRow[] {
return [];
}
/**
* Table total row.
* @returns {ITableRow}
*/
protected get totalRow(): ITableRow {
return this.totalNode(this.report.total);
}
/**
* Retrieves the table rows.
* @returns {ITableRow[]}
*/
public tableRows = (): ITableRow[] => {
return R.compose(
R.unless(R.isEmpty, R.append(this.totalRow)),
R.concat(this.contactsRows)
)([]);
};
// -------------------------
// # Columns.
// -------------------------
/**
* Retrieves the aging table columns.
* @returns {ITableColumn[]}
*/
protected agingTableColumns = (): ITableColumn[] => {
return this.agingPeriods.map((agingPeriod) => {
return {
label: `${agingPeriod.beforeDays} - ${
agingPeriod.toDays || 'And Over'
}`,
key: 'aging_period',
};
});
};
/**
* Retrieves the contact name table column.
* @returns {ITableColumn}
*/
protected contactNameTableColumn = (): ITableColumn => {
return { label: 'Customer name', key: 'customer_name' };
};
/**
* Retrieves the report columns.
* @returns {ITableColumn}
*/
public tableColumns = (): ITableColumn[] => {
return R.compose(this.tableColumnsCellIndexing)([
this.contactNameTableColumn(),
{ label: 'Current', key: 'current' },
...this.agingTableColumns(),
{ label: 'Total', key: 'total' },
]);
};
}

View File

@@ -0,0 +1,4 @@
export enum AgingSummaryRowType {
Contact = 'contact',
Total = 'total',
}

View File

@@ -12,7 +12,7 @@ export const FinancialTable = (Base) =>
* @param {ITableColumn[]} columns * @param {ITableColumn[]} columns
* @returns {ITableColumn[]} * @returns {ITableColumn[]}
*/ */
protected tableColumnsCellIndexing = ( public tableColumnsCellIndexing = (
columns: ITableColumn[] columns: ITableColumn[]
): ITableColumn[] => { ): ITableColumn[] => {
const cellIndex = increment(-1); const cellIndex = increment(-1);

View File

@@ -1,13 +1,13 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import Knex from 'knex'; import { Knex } from 'knex';
import Bluebird from 'bluebird';
import { IVendorCreditAppliedBill } from '@/interfaces'; import { IVendorCreditAppliedBill } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import Bluebird from 'bluebird';
@Service() @Service()
export default class ApplyVendorCreditSyncBills { export default class ApplyVendorCreditSyncBills {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Increment bills credited amount. * Increment bills credited amount.

View File

@@ -18,11 +18,11 @@ function APAgingSummaryBodyJSX({
// #withCurrentOrganization // #withCurrentOrganization
organizationName, organizationName,
}) { }) {
const { isLoading } = useAPAgingSummaryContext(); const { isAPAgingLoading } = useAPAgingSummaryContext();
return ( return (
<FinancialReportBody> <FinancialReportBody>
{isLoading ? ( {isAPAgingLoading ? (
<FinancialSheetSkeleton /> <FinancialSheetSkeleton />
) : ( ) : (
<APAgingSummaryTable organizationName={organizationName} /> <APAgingSummaryTable organizationName={organizationName} />

View File

@@ -20,7 +20,7 @@ export default function APAgingSummaryTable({
}) { }) {
// AP aging summary report content. // AP aging summary report content.
const { const {
APAgingSummary: { tableRows }, APAgingSummary: { table },
isAPAgingLoading, isAPAgingLoading,
} = useAPAgingSummaryContext(); } = useAPAgingSummaryContext();
@@ -36,7 +36,7 @@ export default function APAgingSummaryTable({
> >
<APAgingSummaryDataTable <APAgingSummaryDataTable
columns={columns} columns={columns}
data={tableRows} data={table.rows}
rowClassNames={tableRowTypesToClassnames} rowClassNames={tableRowTypesToClassnames}
noInitialFetch={true} noInitialFetch={true}
sticky={true} sticky={true}
@@ -54,6 +54,24 @@ const APAgingSummaryDataTable = styled(ReportDataTable)`
padding-top: 0.32rem; padding-top: 0.32rem;
padding-bottom: 0.32rem; padding-bottom: 0.32rem;
} }
&:not(.no-results) {
.td {
border-bottom: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
&:not(:first-child) .td {
border-top: 1px solid transparent;
}
&.row_type--total {
font-weight: 500;
.td {
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
}
}
} }
} }
`; `;

View File

@@ -36,12 +36,16 @@ export const getAPAgingSummaryQuerySchema = () => {
.required() .required()
.integer() .integer()
.positive() .positive()
.label('agingBeforeDays'), .label('Aging days before')
.min(1)
.max(500),
agingPeriods: Yup.number() agingPeriods: Yup.number()
.required() .required()
.integer() .integer()
.positive() .positive()
.label('agingPeriods'), .max(12)
.min(1)
.label('Aging periods'),
}); });
}; };

View File

@@ -1,57 +1,20 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import { If, FormattedMessage as T } from '@/components'; import { If } from '@/components';
import { useAPAgingSummaryContext } from './APAgingSummaryProvider';
import { getColumnWidth } from '@/utils';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
import { useAPAgingSummaryContext } from './APAgingSummaryProvider';
import { agingSummaryDynamicColumns } from '../AgingSummary/dynamicColumns';
/** /**
* Retrieve AP aging summary columns. * Retrieve AP aging summary columns.
*/ */
export const useAPAgingSummaryColumns = () => { export const useAPAgingSummaryColumns = () => {
const { const {
APAgingSummary: { tableRows, columns }, APAgingSummary: { table },
} = useAPAgingSummaryContext(); } = useAPAgingSummaryContext();
const agingColumns = React.useMemo(() => { return agingSummaryDynamicColumns(table.columns, table.rows);
return columns.map(
(agingColumn) =>
`${agingColumn.before_days} - ${
agingColumn.to_days || intl.get('and_over')
}`,
);
}, [columns]);
return useMemo(
() => [
{
Header: <T id={'vendor_name'} />,
accessor: 'name',
className: 'vendor_name',
width: 240,
sticky: 'left',
textOverview: true,
},
{
Header: <T id={'current'} />,
accessor: 'current',
className: 'current',
width: getColumnWidth(tableRows, `current`, { minWidth: 120 }),
},
...agingColumns.map((agingColumn, index) => ({
Header: agingColumn,
accessor: `aging-${index}`,
width: getColumnWidth(tableRows, `aging-${index}`, { minWidth: 120 }),
})),
{
Header: <T id={'total'} />,
accessor: 'total',
width: getColumnWidth(tableRows, 'total', { minWidth: 120 }),
},
],
[tableRows, agingColumns],
);
}; };
/** /**

View File

@@ -19,7 +19,10 @@ export default function ReceivableAgingSummaryTable({
organizationName, organizationName,
}) { }) {
// AR aging summary report context. // AR aging summary report context.
const { ARAgingSummary, isARAgingLoading } = useARAgingSummaryContext(); const {
ARAgingSummary: { table },
isARAgingLoading,
} = useARAgingSummaryContext();
// AR aging summary columns. // AR aging summary columns.
const columns = useARAgingSummaryColumns(); const columns = useARAgingSummaryColumns();
@@ -33,7 +36,7 @@ export default function ReceivableAgingSummaryTable({
> >
<ARAgingSummaryDataTable <ARAgingSummaryDataTable
columns={columns} columns={columns}
data={ARAgingSummary.tableRows} data={table.rows}
rowClassNames={tableRowTypesToClassnames} rowClassNames={tableRowTypesToClassnames}
noInitialFetch={true} noInitialFetch={true}
sticky={true} sticky={true}

View File

@@ -35,12 +35,16 @@ export const getARAgingSummaryQuerySchema = () => {
.required() .required()
.integer() .integer()
.positive() .positive()
.label('agingDaysBefore'), .label('Aging days before')
.min(1)
.max(500),
agingPeriods: Yup.number() agingPeriods: Yup.number()
.required() .required()
.integer() .integer()
.positive() .positive()
.label('agingPeriods'), .max(12)
.min(1)
.label('Aging periods'),
}); });
}; };

View File

@@ -1,71 +1,20 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal';
import { useARAgingSummaryContext } from './ARAgingSummaryProvider'; import { useARAgingSummaryContext } from './ARAgingSummaryProvider';
import { If, FormattedMessage as T } from '@/components'; import { If, FormattedMessage as T } from '@/components';
import { getColumnWidth } from '@/utils';
import { Align } from '@/constants';
import FinancialLoadingBar from '../FinancialLoadingBar'; import FinancialLoadingBar from '../FinancialLoadingBar';
import { agingSummaryDynamicColumns } from '../AgingSummary/dynamicColumns';
/** /**
* Retrieve AR aging summary columns. * Retrieve AR aging summary columns.
*/ */
export const useARAgingSummaryColumns = () => { export const useARAgingSummaryColumns = () => {
const { const {
ARAgingSummary: { tableRows, columns }, ARAgingSummary: { table },
} = useARAgingSummaryContext(); } = useARAgingSummaryContext();
const agingColumns = React.useMemo(() => { return agingSummaryDynamicColumns(table.columns, table.rows);
return columns.map(
(agingColumn) =>
`${agingColumn.before_days} - ${
agingColumn.to_days || intl.get('and_over')
}`,
);
}, [columns]);
return React.useMemo(
() => [
{
Header: <T id={'customer_name'} />,
accessor: 'name',
className: 'customer_name',
sticky: 'left',
width: 240,
textOverview: true,
},
{
Header: <T id={'current'} />,
accessor: 'current',
className: 'current',
width: getColumnWidth(tableRows, `current`, {
minWidth: 120,
}),
align: Align.Right
},
...agingColumns.map((agingColumn, index) => ({
Header: agingColumn,
accessor: `aging-${index}`,
width: getColumnWidth(tableRows, `aging-${index}`, {
minWidth: 120,
}),
align: Align.Right
})),
{
Header: <T id={'total'} />,
id: 'total',
accessor: 'total',
className: 'total',
width: getColumnWidth(tableRows, 'total', {
minWidth: 120,
}),
align: Align.Right
},
],
[tableRows, agingColumns],
);
}; };
/** /**

View File

@@ -0,0 +1,74 @@
// @ts-nocheck
import React, { useMemo } from 'react';
import * as R from 'ramda';
import { getColumnWidth } from '@/utils';
import { Align } from '@/constants';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const contactNameAccessor = R.curry((data, column) => ({
key: column.key,
Header: column.label,
accessor: getTableCellValueAccessor(column.cell_index),
sticky: 'left',
width: 240,
textOverview: true,
}));
const currentAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
key: column.key,
Header: column.label,
accessor,
className: column.id,
width: getColumnWidth(data, accessor, { minWidth: 120 }),
align: Align.Right,
};
});
const totalAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
Header: column.label,
id: column.key,
accessor: getTableCellValueAccessor(column.cell_index),
className: column.key,
width: getColumnWidth(data, accessor, { minWidth: 120 }),
align: Align.Right,
};
});
const agingPeriodAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
Header: column.label,
id: `${column.key}-${column.cell_index}`,
accessor,
className: column.key,
width: getColumnWidth(data, accessor, { minWidth: 120 }),
align: Align.Right,
};
});
const dynamicColumnMapper = R.curry((data, column) => {
const totalAccessorColumn = totalAccessor(data);
const currentAccessorColumn = currentAccessor(data);
const customerNameAccessorColumn = contactNameAccessor(data);
const agingPeriodAccessorColumn = agingPeriodAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'total'), totalAccessorColumn),
R.when(R.pathEq(['key'], 'current'), currentAccessorColumn),
R.when(R.pathEq(['key'], 'customer_name'), customerNameAccessorColumn),
R.when(R.pathEq(['key'], 'vendor_name'), customerNameAccessorColumn),
R.when(R.pathEq(['key'], 'aging_period'), agingPeriodAccessorColumn),
)(column);
});
export const agingSummaryDynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};

View File

@@ -138,26 +138,12 @@ export function useARAgingSummaryReport(query, props) {
method: 'get', method: 'get',
url: '/financial_statements/receivable_aging_summary', url: '/financial_statements/receivable_aging_summary',
params: query, params: query,
headers: {
Accept: 'application/json+table',
},
}, },
{ {
select: (res) => ({ select: (res) => res.data,
columns: res.data.columns,
data: res.data.data,
query: res.data.query,
tableRows: ARAgingSummaryTableRowsMapper({
customers: res.data.data.customers,
total: res.data.data.total,
columns: res.data.columns,
}),
}),
defaultData: {
data: {
customers: [],
total: {},
},
columns: [],
tableRows: [],
},
...props, ...props,
}, },
); );
@@ -173,26 +159,12 @@ export function useAPAgingSummaryReport(query, props) {
method: 'get', method: 'get',
url: '/financial_statements/payable_aging_summary', url: '/financial_statements/payable_aging_summary',
params: query, params: query,
headers: {
Accept: 'application/json+table',
},
}, },
{ {
select: (res) => ({ select: (res) => res.data,
columns: res.data.columns,
data: res.data.data,
query: res.data.query,
tableRows: APAgingSummaryTableRowsMapper({
vendors: res.data.data.vendors,
total: res.data.data.total,
columns: res.data.columns,
}),
}),
defaultData: {
data: {
vendors: [],
total: {},
},
columns: [],
tableRows: [],
},
...props, ...props,
}, },
); );