Compare commits

...

38 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
eff8b41720 Merge pull request #519 from bigcapitalhq/change-settings-value-colum-to-text
fix: alter value column of the settings table to text instead of string
2024-07-04 09:05:17 +02:00
Ahmed Bouhuolia
632cc3d72e fix: alter value column of the settings table to text instead of string 2024-07-04 08:58:41 +02:00
Ahmed Bouhuolia
3e437a041c Merge pull request #518 from bigcapitalhq/BIG-213
fix: Tax rate not saving on creating a new invoice
2024-06-24 10:47:21 +02:00
Ahmed Bouhuolia
e783cfeafa fix: Tax rate not saving on creating a new invoice 2024-06-24 10:46:19 +02:00
Ahmed Bouhuolia
5dde7f5584 Merge pull request #516 from bigcapitalhq/BIG-212
fix: Reorder 'debit' and 'credit' columns
2024-06-24 10:44:28 +02:00
Ahmed Bouhuolia
8e0911ec85 fix: Reorder 'debit' and 'credit' columns 2024-06-24 10:43:34 +02:00
Ahmed Bouhuolia
7b4afd3859 Update .env.example 2024-06-17 18:30:13 +02:00
Ahmed Bouhuolia
590715037b chore: dump CHANGELOG.md 2024-06-17 15:33:49 +02:00
Ahmed Bouhuolia
1e53a8e85e Merge pull request #506 from bigcapitalhq/BIG-206
feat: Setting up the date format in the whole system dates
2024-06-17 12:53:31 +02:00
Ahmed Bouhuolia
2ad77103ac feat: cashflow tranasction date format 2024-06-17 12:50:31 +02:00
Ahmed Bouhuolia
c1fc70863b Merge pull request #497 from bigcapitalhq/BIG-195
fix: Disable email confirmation does not work with invited users
2024-06-17 10:34:33 +02:00
Ahmed Bouhuolia
125dff8376 feat: format created at date 2024-06-17 10:27:02 +02:00
Ahmed Bouhuolia
84da7b7df5 Merge pull request #509 from bigcapitalhq/BIG-193
feat: Migrating to Envoy proxy instead of Nginx
2024-06-17 09:22:23 +02:00
Ahmed Bouhuolia
4c82f6f8ad feat: Migrating to Envoy proxy instead of Nginx 2024-06-15 11:54:19 +02:00
Ahmed Bouhuolia
0d7aad5448 Merge pull request #508 from bigcapitalhq/BIG-142
fix: add space between buttons on floating actions bar
2024-06-14 08:29:45 +02:00
Ahmed Bouhuolia
74b74a2722 fix: add space between buttons on floating actions bar 2024-06-14 08:27:30 +02:00
Ahmed Bouhuolia
3a0a0db8a7 feat: setting up the date format in the whole system dates 2024-06-12 19:43:42 +02:00
Ahmed Bouhuolia
265ea9ca48 Merge pull request #501 from bigcapitalhq/BIG-202
fix: Balance sheet and P/L nested accounts
2024-06-12 13:06:37 +02:00
Ahmed Bouhuolia
cfd37f8894 fix: Balance sheet and P/L nested accounts 2024-06-12 13:05:02 +02:00
Ahmed Bouhuolia
d1caa5c5ce fix: Disable email confirmation does not work with invited users 2024-06-10 15:59:33 +02:00
Ahmed Bouhuolia
d998d716b7 Merge pull request #496 from bigcapitalhq/fix-payment-receive-attachments
fix: Edit the payment received transactions with attachments
2024-06-10 13:41:44 +02:00
Ahmed Bouhuolia
031ccc4a0b fix: Edit the payment received transactions with attachments 2024-06-10 13:41:10 +02:00
Ahmed Bouhuolia
e4f61823b3 Merge pull request #485 from bigcapitalhq/BIG-186
fix: Closing balance in general ledger report does not sum the negative figures
2024-06-10 08:17:01 +02:00
Ahmed Bouhuolia
1cbc1c056f feat: general ledger filter nodes 2024-06-10 08:08:47 +02:00
Ahmed Bouhuolia
4d4ef54c56 Merge pull request #494 from bigcapitalhq/BIG-192
fix: Concurrency controlling multiple processes in Bigcapital CLI commands
2024-06-09 22:54:02 +02:00
Ahmed Bouhuolia
f7fcfefc78 fix: Concurrency controlling multiple processes in Bigcapital CLI commands 2024-06-09 22:52:56 +02:00
Ahmed Bouhuolia
858f347fd4 Merge pull request #493 from bigcapitalhq/BIG-198
fix: Something wrong in uploading uncategorized bank transactions
2024-06-09 21:30:32 +02:00
Ahmed Bouhuolia
4d73b59cf3 fix: Something wrong in uploading uncategorized bank transactions 2024-06-09 21:30:07 +02:00
Ahmed Bouhuolia
bc67f0cca8 fix: increment/decrement the uncategorized transactios on accounts 2024-06-09 21:05:43 +02:00
Ahmed Bouhuolia
ef2d1ff141 feat: Add COGS type to cash transactions categorization 2024-06-09 21:05:19 +02:00
Ahmed Bouhuolia
dc4cdb2a8f fix: Assign branch in categorize bank transaction 2024-06-09 20:05:15 +02:00
Ahmed Bouhuolia
8b99e0938d fix: remove un-used code 2024-06-06 18:50:24 +02:00
Ahmed Bouhuolia
94192bfc29 fix: doctype general ledger 2024-06-06 18:48:33 +02:00
Ahmed Bouhuolia
708a4dda9e chore: remove the console.log 2024-06-06 18:44:19 +02:00
Ahmed Bouhuolia
10fcf94c92 feat: general ledger closing balance with accounts row 2024-06-06 18:42:07 +02:00
Ahmed Bouhuolia
5dbfd36415 feat: optimize the style of general ledger sub-accounts rows 2024-06-05 22:42:12 +02:00
Ahmed Bouhuolia
044f11ff74 feat: general ledger sub-accounts 2024-06-05 21:45:01 +02:00
Ahmed Bouhuolia
6afe1a09c6 fix: Closing balance in general ledger report does not sum the negative figures. 2024-06-04 21:26:46 +02:00
106 changed files with 1281 additions and 536 deletions

View File

@@ -84,8 +84,8 @@ LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET= LEMONSQUEEZY_WEBHOOK_SECRET=
# S3 documents and attachments # S3 documents and attachments
S3_REGION= S3_REGION=US
S3_ACCESS_KEY_ID= S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY= S3_SECRET_ACCESS_KEY=
S3_ENDPOINT= S3_ENDPOINT=
S3_BUCKET= S3_BUCKET=

View File

@@ -2,6 +2,14 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
## [v0.17.5] - 17-06-2024
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
* fix: add space between buttons on floating actions bar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/508
* feat: Migrating to Envoy proxy instead of Nginx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/509
* fix: Disable email confirmation does not work with invited users by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/497
* feat: Setting up the date format in the whole system dates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/506
## [0.17.0] - 04-06-2024 ## [0.17.0] - 04-06-2024
### New ### New

View File

@@ -3,24 +3,17 @@
version: '3.3' version: '3.3'
services: services:
nginx: proxy:
container_name: bigcapital-nginx-gateway image: envoyproxy/envoy:v1.30-latest
build: depends_on:
context: ./docker/nginx - server
args: - webapp
- SERVER_PROXY_PORT=3000
- WEB_SSL=false
- SELF_SIGNED=false
volumes:
- ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs
ports: ports:
- '${PUBLIC_PROXY_PORT:-80}:80' - '${PUBLIC_PROXY_PORT:-80}:80'
- '${PUBLIC_PROXY_SSL_PORT:-443}:443' - '${PUBLIC_PROXY_SSL_PORT:-443}:443'
tty: true tty: true
depends_on: volumes:
- server - ./docker/envoy/envoy.yaml:/etc/envoy/envoy.yaml
- webapp
restart: on-failure restart: on-failure
networks: networks:
- bigcapital_network - bigcapital_network
@@ -46,6 +39,8 @@ services:
- mongo - mongo
- redis - redis
restart: on-failure restart: on-failure
networks:
- bigcapital_network
environment: environment:
# Mail # Mail
- MAIL_HOST=${MAIL_HOST} - MAIL_HOST=${MAIL_HOST}
@@ -127,8 +122,6 @@ services:
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_ENDPOINT=${S3_ENDPOINT} - S3_ENDPOINT=${S3_ENDPOINT}
- S3_BUCKET=${S3_BUCKET} - S3_BUCKET=${S3_BUCKET}
networks:
- bigcapital_network
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration

62
docker/envoy/envoy.yaml Normal file
View File

@@ -0,0 +1,62 @@
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ['*']
routes:
- match:
prefix: '/api'
route:
cluster: dynamic_server
- match:
prefix: '/'
route:
cluster: webapp
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: dynamic_server
connect_timeout: 0.25s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: dynamic_server
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: server
port_value: 3000
- name: webapp
connect_timeout: 0.25s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: webapp
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: webapp
port_value: 80

View File

@@ -1,21 +0,0 @@
FROM nginx:1.11
RUN mkdir /etc/nginx/sites-available && rm /etc/nginx/conf.d/default.conf
ADD nginx.conf /etc/nginx/
COPY scripts /root/scripts/
COPY certs /etc/ssl/
COPY sites /etc/nginx/templates
ARG SERVER_PROXY_PORT=3000
ARG WEB_SSL=false
ARG SELF_SIGNED=false
ENV SERVER_PROXY_PORT=$SERVER_PROXY_PORT
ENV WEB_SSL=$WEB_SSL
ENV SELF_SIGNED=$SELF_SIGNED
RUN /bin/bash /root/scripts/build-nginx.sh
CMD nginx

View File

@@ -1,33 +0,0 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
daemon off;
events {
worker_connections 2048;
use epoll;
}
http {
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
client_max_body_size 20M;
open_file_cache max=100;
gzip on;
gzip_disable "msie6";
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-available/*;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}

View File

@@ -1,9 +0,0 @@
#!/bin/bash
for conf in /etc/nginx/templates/*.conf; do
mv $conf "/etc/nginx/sites-available/"$(basename $conf) > /dev/null
done
for template in /etc/nginx/templates/*.template; do
envsubst < $template > "/etc/nginx/sites-available/"$(basename $template)".conf"
done

View File

@@ -1,16 +0,0 @@
server {
listen 80 default_server;
location /api {
proxy_pass http://server:${SERVER_PROXY_PORT};
}
location / {
proxy_pass http://webapp;
}
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt/;
log_not_found off;
}
}

View File

@@ -25,6 +25,7 @@
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0", "@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@supercharge/promise-pool": "^3.2.0",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/i18n": "^0.8.7", "@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1", "@types/knex": "^0.16.1",

View File

@@ -5,7 +5,7 @@ import DashboardService from '@/services/Dashboard/DashboardService';
@Service() @Service()
export default class DashboardMetaController { export default class DashboardMetaController {
@Inject() @Inject()
dashboardService: DashboardService; private dashboardService: DashboardService;
/** /**
* Constructor router. * Constructor router.

View File

@@ -4,6 +4,7 @@ import color from 'colorette';
import argv from 'getopts'; import argv from 'getopts';
import Knex from 'knex'; import Knex from 'knex';
import { knexSnakeCaseMappers } from 'objection'; import { knexSnakeCaseMappers } from 'objection';
import { PromisePool } from '@supercharge/promise-pool';
import '../before'; import '../before';
import config from '../config'; import config from '../config';
@@ -28,7 +29,7 @@ function initSystemKnex() {
}); });
} }
function initTenantKnex(organizationId) { function initTenantKnex(organizationId: string = '') {
return Knex({ return Knex({
client: config.tenant.db_client, client: config.tenant.db_client,
connection: { connection: {
@@ -71,10 +72,12 @@ function getAllSystemTenants(knex) {
return knex('tenants'); return knex('tenants');
} }
function getAllInitializedSystemTenants(knex) { function getAllInitializedTenants(knex) {
return knex('tenants').whereNotNull('initializedAt'); return knex('tenants').whereNotNull('initializedAt');
} }
const MIGRATION_CONCURRENCY = 10;
// module.exports = { // module.exports = {
// log, // log,
// success, // success,
@@ -91,6 +94,7 @@ function getAllInitializedSystemTenants(knex) {
// - bigcapital tenants:migrate:make // - bigcapital tenants:migrate:make
// - bigcapital system:migrate:make // - bigcapital system:migrate:make
// - bigcapital tenants:list // - bigcapital tenants:list
// - bigcapital tenants:list --all
commander commander
.command('system:migrate:rollback') .command('system:migrate:rollback')
@@ -149,10 +153,13 @@ commander
commander commander
.command('tenants:list') .command('tenants:list')
.description('Retrieve a list of all system tenants databases.') .description('Retrieve a list of all system tenants databases.')
.option('-a, --all', 'All tenants even are not initialized.')
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex); const tenants = cmd?.all
? await getAllSystemTenants(sysKnex)
: await getAllInitializedTenants(sysKnex);
tenants.forEach((tenant) => { tenants.forEach((tenant) => {
const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`; const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`;
@@ -183,18 +190,20 @@ commander
commander commander
.command('tenants:migrate:latest') .command('tenants:migrate:latest')
.description('Migrate all tenants or the given tenant id.') .description('Migrate all tenants or the given tenant id.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.') .option(
'-t, --tenant_id [tenant_id]',
'Which organization id do you migrate.'
)
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllInitializedSystemTenants(sysKnex); const tenants = await getAllInitializedTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`); exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
} }
// Validate the tenant id exist first of all. // Validate the tenant id exist first of all.
const migrateOpers = [];
const migrateTenant = async (organizationId) => { const migrateTenant = async (organizationId) => {
try { try {
const tenantKnex = await initTenantKnex(organizationId); const tenantKnex = await initTenantKnex(organizationId);
@@ -216,17 +225,17 @@ commander
} }
}; };
if (!cmd.tenant_id) { if (!cmd.tenant_id) {
tenants.forEach((tenant) => { await PromisePool.withConcurrency(MIGRATION_CONCURRENCY)
const oper = migrateTenant(tenant.organizationId); .for(tenants)
migrateOpers.push(oper); .process((tenant, index, pool) => {
}); return migrateTenant(tenant.organizationId);
})
.then(() => {
success('All tenants are migrated.');
});
} else { } else {
const oper = migrateTenant(cmd.tenant_id); await migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
} }
Promise.all(migrateOpers).then(() => {
success('All tenants are migrated.');
});
} catch (error) { } catch (error) {
exit(error); exit(error);
} }
@@ -235,19 +244,21 @@ commander
commander commander
.command('tenants:migrate:rollback') .command('tenants:migrate:rollback')
.description('Rollback the last batch of tenants migrations.') .description('Rollback the last batch of tenants migrations.')
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.') .option(
'-t, --tenant_id [tenant_id]',
'Which organization id do you migrate.'
)
.action(async (cmd) => { .action(async (cmd) => {
try { try {
const sysKnex = await initSystemKnex(); const sysKnex = await initSystemKnex();
const tenants = await getAllSystemTenants(sysKnex); const tenants = await getAllInitializedTenants(sysKnex);
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId); const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) { if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
exit(`The given tenant id ${cmd.tenant_id} is not exists.`); exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
} }
const migrateOpers = []; const migrateTenant = async (organizationId: string) => {
const migrateTenant = async (organizationId) => {
try { try {
const tenantKnex = await initTenantKnex(organizationId); const tenantKnex = await initTenantKnex(organizationId);
const [batchNo, _log] = await tenantKnex.migrate.rollback(); const [batchNo, _log] = await tenantKnex.migrate.rollback();
@@ -268,17 +279,17 @@ commander
}; };
if (!cmd.tenant_id) { if (!cmd.tenant_id) {
tenants.forEach((tenant) => { await PromisePool.withConcurrency(MIGRATION_CONCURRENCY)
const oper = migrateTenant(tenant.organizationId); .for(tenants)
migrateOpers.push(oper); .process((tenant, index, pool) => {
}); return migrateTenant(tenant.organizationId);
})
.then(() => {
success('All tenants are rollbacked.');
});
} else { } else {
const oper = migrateTenant(cmd.tenant_id); await migrateTenant(cmd.tenant_id);
migrateOpers.push(oper);
} }
Promise.all(migrateOpers).then(() => {
success('All tenants are rollbacked.');
});
} catch (error) { } catch (error) {
exit(error); exit(error);
} }

View File

@@ -0,0 +1,11 @@
exports.up = function (knex) {
return knex.schema.table('settings', (table) => {
table.text('value').alter();
});
};
exports.down = (knex) => {
return knex.schema.table('settings', (table) => {
table.string('value').alter();
});
};

View File

@@ -56,6 +56,8 @@ export interface IGeneralLedgerSheetAccount {
transactions: IGeneralLedgerSheetAccountTransaction[]; transactions: IGeneralLedgerSheetAccountTransaction[];
openingBalance: IGeneralLedgerSheetAccountBalance; openingBalance: IGeneralLedgerSheetAccountBalance;
closingBalance: IGeneralLedgerSheetAccountBalance; closingBalance: IGeneralLedgerSheetAccountBalance;
closingBalanceSubaccounts?: IGeneralLedgerSheetAccountBalance;
children?: IGeneralLedgerSheetAccount[];
} }
export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[]; export type IGeneralLedgerSheetData = IGeneralLedgerSheetAccount[];

View File

@@ -149,13 +149,19 @@ export class Transformer {
return this.excludeAttributes().length > 0; return this.excludeAttributes().length > 0;
}; };
private dateFormat = 'YYYY MMM DD';
setDateFormat(format: string) {
this.dateFormat = format;
}
/** /**
* *
* @param date * @param date
* @returns * @returns
*/ */
protected formatDate(date) { protected formatDate(date) {
return date ? moment(date).format('YYYY/MM/DD') : ''; return date ? moment(date).format(this.dateFormat) : '';
} }
/** /**
@@ -193,6 +199,7 @@ export class Transformer {
) { ) {
transformer.setOptions(options); transformer.setOptions(options);
transformer.setContext(this.context); transformer.setContext(this.context);
transformer.setDateFormat(this.dateFormat);
return transformer.work(obj); return transformer.work(obj);
} }

View File

@@ -24,6 +24,17 @@ export class TransformerInjectable {
}; };
} }
/**
* Retrieves the given tenatn date format.
* @param {number} tenantId
* @returns {string}
*/
async getTenantDateFormat(tenantId: number) {
const metadata = await TenantMetadata.query().findOne('tenantId', tenantId);
return metadata.dateFormat;
}
/** /**
* Transformes the given transformer after inject the tenant context. * Transformes the given transformer after inject the tenant context.
* @param {number} tenantId * @param {number} tenantId
@@ -41,7 +52,11 @@ export class TransformerInjectable {
if (!isNull(tenantId)) { if (!isNull(tenantId)) {
const context = await this.getApplicationContext(tenantId); const context = await this.getApplicationContext(tenantId);
transformer.setContext(context); transformer.setContext(context);
const dateFormat = await this.getTenantDateFormat(tenantId);
transformer.setDateFormat(dateFormat);
} }
transformer.setOptions(options); transformer.setOptions(options);
return transformer.work(object); return transformer.work(object);

View File

@@ -104,10 +104,10 @@ export default class UncategorizedCashflowTransaction extends mixin(
*/ */
private async updateUncategorizedTransactionCount( private async updateUncategorizedTransactionCount(
queryContext: QueryContext, queryContext: QueryContext,
increment: boolean increment: boolean,
amount: number = 1
) { ) {
const operation = increment ? 'increment' : 'decrement'; const operation = increment ? 'increment' : 'decrement';
const amount = increment ? 1 : -1;
await Account.query(queryContext.transaction) await Account.query(queryContext.transaction)
.findById(this.accountId) .findById(this.accountId)

View File

@@ -51,7 +51,7 @@ export default class Ledger implements ILedger {
/** /**
* Filters entries by the given accounts ids then returns a new ledger. * Filters entries by the given accounts ids then returns a new ledger.
* @param {number[]} accountIds * @param {number[]} accountIds
* @returns {ILedger} * @returns {ILedger}
*/ */
public whereAccountsIds(accountIds: number[]): ILedger { public whereAccountsIds(accountIds: number[]): ILedger {
@@ -274,4 +274,14 @@ export default class Ledger implements ILedger {
const entries = Ledger.mappingTransactions(transactions); const entries = Ledger.mappingTransactions(transactions);
return new Ledger(entries); return new Ledger(entries);
} }
/**
* Retrieve the transaction amount.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
* @param {string} normal - Credit or debit.
*/
static getAmount(credit: number, debit: number, normal: string) {
return normal === 'credit' ? credit - debit : debit - credit;
}
} }

View File

@@ -1,6 +1,6 @@
import { Container, Inject } from 'typedi'; import { Container, Inject } from 'typedi';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { Tenant } from '@/system/models'; import { SystemUser, Tenant } from '@/system/models';
import { import {
IAuthSignedInEventPayload, IAuthSignedInEventPayload,
IAuthSigningInEventPayload, IAuthSigningInEventPayload,
@@ -64,7 +64,9 @@ export class AuthSigninService {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
// Finds the user of the given email address. // Finds the user of the given email address.
const user = await systemUserRepository.findOneByEmail(email); const user = await SystemUser.query()
.findOne('email', email)
.modify('inviteAccepted');
// Validate the given email and password. // Validate the given email and password.
await this.validateSignIn(user, email, password); await this.validateSignIn(user, email, password);

View File

@@ -7,7 +7,12 @@ export class CashflowTransactionTransformer extends Transformer {
* @returns {string[]} * @returns {string[]}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['formattedAmount', 'transactionTypeFormatted']; return [
'formattedAmount',
'transactionTypeFormatted',
'formattedDate',
'formattedCreatedAt',
];
}; };
/** /**
@@ -24,10 +29,28 @@ export class CashflowTransactionTransformer extends Transformer {
/** /**
* Formatted transaction type. * Formatted transaction type.
* @param transaction * @param transaction
* @returns {string} * @returns {string}
*/ */
protected transactionTypeFormatted = (transaction) => { protected transactionTypeFormatted = (transaction) => {
return this.context.i18n.__(transaction.transactionTypeFormatted); return this.context.i18n.__(transaction.transactionTypeFormatted);
} };
/**
* Retrieve the formatted transaction date.
* @param invoice
* @returns {string}
*/
protected formattedDate = (invoice): string => {
return this.formatDate(invoice.date);
};
/**
* Retrieve the formatted created at date.
* @param invoice
* @returns {string}
*/
protected formattedCreatedAt = (invoice): string => {
return this.formatDate(invoice.createdAt);
};
} }

View File

@@ -12,7 +12,6 @@ import { Knex } from 'knex';
import { transformCategorizeTransToCashflow } from './utils'; import { transformCategorizeTransToCashflow } from './utils';
import { CommandCashflowValidator } from './CommandCasflowValidator'; import { CommandCashflowValidator } from './CommandCasflowValidator';
import NewCashflowTransactionService from './NewCashflowTransactionService'; import NewCashflowTransactionService from './NewCashflowTransactionService';
import { TransferAuthorizationGuaranteeDecision } from 'plaid';
@Service() @Service()
export class CategorizeCashflowTransaction { export class CategorizeCashflowTransaction {

View File

@@ -68,7 +68,11 @@ export const CASHFLOW_TRANSACTION_TYPE_META = {
[`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: { [`${CASHFLOW_TRANSACTION_TYPE.OTHER_EXPENSE}`]: {
type: 'OtherExpense', type: 'OtherExpense',
direction: CASHFLOW_DIRECTION.OUT, direction: CASHFLOW_DIRECTION.OUT,
creditType: [ACCOUNT_TYPE.EXPENSE, ACCOUNT_TYPE.OTHER_EXPENSE], creditType: [
ACCOUNT_TYPE.EXPENSE,
ACCOUNT_TYPE.OTHER_EXPENSE,
ACCOUNT_TYPE.COST_OF_GOODS_SOLD,
],
}, },
}; };

View File

@@ -1,4 +1,4 @@
import { upperFirst, camelCase, omit } from 'lodash'; import { upperFirst, camelCase } from 'lodash';
import { import {
CASHFLOW_TRANSACTION_TYPE, CASHFLOW_TRANSACTION_TYPE,
CASHFLOW_TRANSACTION_TYPE_META, CASHFLOW_TRANSACTION_TYPE_META,
@@ -6,7 +6,6 @@ import {
} from './constants'; } from './constants';
import { import {
ICashflowNewCommandDTO, ICashflowNewCommandDTO,
ICashflowTransaction,
ICategorizeCashflowTransactioDTO, ICategorizeCashflowTransactioDTO,
IUncategorizedCashflowTransaction, IUncategorizedCashflowTransaction,
} from '@/interfaces'; } from '@/interfaces';
@@ -42,8 +41,8 @@ export const getCashflowAccountTransactionsTypes = () => {
/** /**
* Tranasformes the given uncategorized transaction and categorized DTO * Tranasformes the given uncategorized transaction and categorized DTO
* to cashflow create DTO. * to cashflow create DTO.
* @param {IUncategorizedCashflowTransaction} uncategorizeModel * @param {IUncategorizedCashflowTransaction} uncategorizeModel
* @param {ICategorizeCashflowTransactioDTO} categorizeDTO * @param {ICategorizeCashflowTransactioDTO} categorizeDTO
* @returns {ICashflowNewCommandDTO} * @returns {ICashflowNewCommandDTO}
*/ */
export const transformCategorizeTransToCashflow = ( export const transformCategorizeTransToCashflow = (
@@ -62,6 +61,7 @@ export const transformCategorizeTransToCashflow = (
transactionNumber: categorizeDTO.transactionNumber, transactionNumber: categorizeDTO.transactionNumber,
transactionType: categorizeDTO.transactionType, transactionType: categorizeDTO.transactionType,
uncategorizedTransactionId: uncategorizeModel.id, uncategorizedTransactionId: uncategorizeModel.id,
branchId: categorizeDTO?.branchId,
publish: true, publish: true,
}; };
}; };

View File

@@ -13,6 +13,8 @@ export class CreditNoteTransformer extends Transformer {
return [ return [
'formattedCreditsRemaining', 'formattedCreditsRemaining',
'formattedCreditNoteDate', 'formattedCreditNoteDate',
'formattedCreatedAt',
'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'formattedCreditsUsed', 'formattedCreditsUsed',
'formattedSubtotal', 'formattedSubtotal',
@@ -30,6 +32,15 @@ export class CreditNoteTransformer extends Transformer {
return this.formatDate(credit.creditNoteDate); return this.formatDate(credit.creditNoteDate);
}; };
/**
* Retrieve formatted created at date.
* @param credit
* @returns {string}
*/
protected formattedCreatedAt = (credit): string => {
return this.formatDate(credit.createdAt);
};
/** /**
* Retrieve formatted invoice amount. * Retrieve formatted invoice amount.
* @param {ICreditNote} credit * @param {ICreditNote} credit

View File

@@ -15,6 +15,7 @@ export class ExpenseTransfromer extends Transformer {
'formattedLandedCostAmount', 'formattedLandedCostAmount',
'formattedAllocatedCostAmount', 'formattedAllocatedCostAmount',
'formattedDate', 'formattedDate',
'formattedCreatedAt',
'categories', 'categories',
'attachments', 'attachments',
]; ];
@@ -62,6 +63,15 @@ export class ExpenseTransfromer extends Transformer {
return this.formatDate(expense.paymentDate); return this.formatDate(expense.paymentDate);
}; };
/**
* Retrieve formatted created at date.
* @param {IExpense} expense
* @returns {string}
*/
protected formattedCreatedAt = (expense: IExpense): string => {
return this.formatDate(expense.createdAt);
}
/** /**
* Retrieves the transformed expense categories. * Retrieves the transformed expense categories.
* @param {IExpense} expense * @param {IExpense} expense

View File

@@ -20,6 +20,8 @@ import { BalanceSheetPercentage } from './BalanceSheetPercentage';
import { BalanceSheetSchema } from './BalanceSheetSchema'; import { BalanceSheetSchema } from './BalanceSheetSchema';
import { BalanceSheetBase } from './BalanceSheetBase'; import { BalanceSheetBase } from './BalanceSheetBase';
import { BalanceSheetQuery } from './BalanceSheetQuery'; import { BalanceSheetQuery } from './BalanceSheetQuery';
import { flatToNestedArray } from '@/utils';
import BalanceSheetRepository from './BalanceSheetRepository';
export const BalanceSheetAccounts = (Base: any) => export const BalanceSheetAccounts = (Base: any) =>
class extends R.compose( class extends R.compose(
@@ -56,6 +58,11 @@ export const BalanceSheetAccounts = (Base: any) =>
*/ */
readonly i18n: any; readonly i18n: any;
/**
* Balance sheet repository.
*/
readonly repository: BalanceSheetRepository;
/** /**
* Retrieve the accounts node of accounts types. * Retrieve the accounts node of accounts types.
* @param {string} accountsTypes * @param {string} accountsTypes
@@ -78,8 +85,12 @@ export const BalanceSheetAccounts = (Base: any) =>
private reportSchemaAccountNodeMapper = ( private reportSchemaAccountNodeMapper = (
account: IAccount account: IAccount
): IBalanceSheetAccountNode => { ): IBalanceSheetAccountNode => {
const childrenAccountsIds = this.repository.accountsGraph.dependenciesOf(
account.id
);
const accountIds = R.uniq(R.append(account.id, childrenAccountsIds));
const total = this.repository.totalAccountsLedger const total = this.repository.totalAccountsLedger
.whereAccountId(account.id) .whereAccountsIds(accountIds)
.getClosingBalance(); .getClosingBalance();
return { return {
@@ -128,8 +139,19 @@ export const BalanceSheetAccounts = (Base: any) =>
private getAccountsNodesByAccountTypes = ( private getAccountsNodesByAccountTypes = (
accountsTypes: string[] accountsTypes: string[]
): IBalanceSheetAccountNode[] => { ): IBalanceSheetAccountNode[] => {
// Retrieves accounts from the given defined node account types.
const accounts = this.getAccountsByAccountTypes(accountsTypes); const accounts = this.getAccountsByAccountTypes(accountsTypes);
return R.map(this.reportSchemaAccountNodeComposer, accounts);
// Converts the flatten accounts to tree.
const accountsTree = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
});
// Maps over the accounts tree.
return this.mapNodesDeep(
accountsTree,
this.reportSchemaAccountNodeComposer
);
}; };
/** /**

View File

@@ -1,6 +1,4 @@
import * as R from 'ramda'; import * as R from 'ramda';
import { FinancialPreviousPeriod } from '../FinancialPreviousPeriod';
import { FinancialHorizTotals } from '../FinancialHorizTotals';
import { FinancialSheetStructure } from '../FinancialSheetStructure'; import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { import {
BALANCE_SHEET_SCHEMA_NODE_TYPE, BALANCE_SHEET_SCHEMA_NODE_TYPE,

View File

@@ -3,7 +3,6 @@ import * as R from 'ramda';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
IAccount,
IAccountTransactionsGroupBy, IAccountTransactionsGroupBy,
IBalanceSheetQuery, IBalanceSheetQuery,
ILedger, ILedger,
@@ -12,7 +11,6 @@ import { transformToMapBy } from 'utils';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import { BalanceSheetQuery } from './BalanceSheetQuery'; import { BalanceSheetQuery } from './BalanceSheetQuery';
import { FinancialDatePeriods } from '../FinancialDatePeriods'; import { FinancialDatePeriods } from '../FinancialDatePeriods';
import { ACCOUNT_PARENT_TYPE, ACCOUNT_TYPE } from '@/data/AccountTypes';
import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome'; import { BalanceSheetRepositoryNetIncome } from './BalanceSheetRepositoryNetIncome';
@Service() @Service()
@@ -40,6 +38,11 @@ export default class BalanceSheetRepository extends R.compose(
*/ */
public accounts: any; public accounts: any;
/**
* @param {}
*/
public accountsGraph: any;
/** /**
* *
*/ */
@@ -163,6 +166,8 @@ export default class BalanceSheetRepository extends R.compose(
*/ */
public asyncInitialize = async () => { public asyncInitialize = async () => {
await this.initAccounts(); await this.initAccounts();
await this.initAccountsGraph();
await this.initAccountsTotalLedger(); await this.initAccountsTotalLedger();
// Date periods. // Date periods.
@@ -204,6 +209,15 @@ export default class BalanceSheetRepository extends R.compose(
this.accountsByParentType = transformToMapBy(accounts, 'accountParentType'); this.accountsByParentType = transformToMapBy(accounts, 'accountParentType');
}; };
/**
* Initialize accounts graph.
*/
public initAccountsGraph = async () => {
const { Account } = this.models;
this.accountsGraph = Account.toDependencyGraph(this.accounts);
};
// ---------------------------- // ----------------------------
// # Closing Total // # Closing Total
// ---------------------------- // ----------------------------

View File

@@ -1,29 +1,31 @@
import { isEmpty, get, last, sumBy } from 'lodash'; import { isEmpty, get, last, sumBy, first, head } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import { import {
IGeneralLedgerSheetQuery, IGeneralLedgerSheetQuery,
IGeneralLedgerSheetAccount, IGeneralLedgerSheetAccount,
IGeneralLedgerSheetAccountBalance, IGeneralLedgerSheetAccountBalance,
IGeneralLedgerSheetAccountTransaction, IGeneralLedgerSheetAccountTransaction,
IAccount, IAccount,
IJournalPoster, ILedgerEntry,
IJournalEntry,
IContact,
} from '@/interfaces'; } from '@/interfaces';
import FinancialSheet from '../FinancialSheet'; import FinancialSheet from '../FinancialSheet';
import moment from 'moment'; import { GeneralLedgerRepository } from './GeneralLedgerRepository';
import { FinancialSheetStructure } from '../FinancialSheetStructure';
import { flatToNestedArray } from '@/utils';
import Ledger from '@/services/Accounting/Ledger';
import { calculateRunningBalance } from './_utils';
/** /**
* General ledger sheet. * General ledger sheet.
*/ */
export default class GeneralLedgerSheet extends FinancialSheet { export default class GeneralLedgerSheet extends R.compose(
tenantId: number; FinancialSheetStructure
accounts: IAccount[]; )(FinancialSheet) {
query: IGeneralLedgerSheetQuery; private query: IGeneralLedgerSheetQuery;
openingBalancesJournal: IJournalPoster; private baseCurrency: string;
transactions: IJournalPoster; private i18n: any;
contactsMap: Map<number, IContact>; private repository: GeneralLedgerRepository;
baseCurrency: string;
i18n: any;
/** /**
* Constructor method. * Constructor method.
@@ -34,63 +36,59 @@ export default class GeneralLedgerSheet extends FinancialSheet {
* @param {IJournalPoster} closingBalancesJournal - * @param {IJournalPoster} closingBalancesJournal -
*/ */
constructor( constructor(
tenantId: number,
query: IGeneralLedgerSheetQuery, query: IGeneralLedgerSheetQuery,
accounts: IAccount[], repository: GeneralLedgerRepository,
contactsByIdMap: Map<number, IContact>,
transactions: IJournalPoster,
openingBalancesJournal: IJournalPoster,
baseCurrency: string,
i18n i18n
) { ) {
super(); super();
this.tenantId = tenantId;
this.query = query; this.query = query;
this.numberFormat = this.query.numberFormat; this.numberFormat = this.query.numberFormat;
this.accounts = accounts; this.repository = repository;
this.contactsMap = contactsByIdMap; this.baseCurrency = this.repository.tenant.metadata.currencyCode;
this.transactions = transactions;
this.openingBalancesJournal = openingBalancesJournal;
this.baseCurrency = baseCurrency;
this.i18n = i18n; this.i18n = i18n;
} }
/**
* Retrieve the transaction amount.
* @param {number} credit - Credit amount.
* @param {number} debit - Debit amount.
* @param {string} normal - Credit or debit.
*/
getAmount(credit: number, debit: number, normal: string) {
return normal === 'credit' ? credit - debit : debit - credit;
}
/** /**
* Entry mapper. * Entry mapper.
* @param {IJournalEntry} entry - * @param {ILedgerEntry} entry -
* @return {IGeneralLedgerSheetAccountTransaction} * @return {IGeneralLedgerSheetAccountTransaction}
*/ */
entryReducer( private getEntryRunningBalance(
entries: IGeneralLedgerSheetAccountTransaction[], entry: ILedgerEntry,
entry: IJournalEntry, openingBalance: number,
openingBalance: number runningBalance?: number
): IGeneralLedgerSheetAccountTransaction[] { ): number {
const lastEntry = last(entries); const lastRunningBalance = runningBalance || openingBalance;
const contact = this.contactsMap.get(entry.contactId); const amount = Ledger.getAmount(
const amount = this.getAmount(
entry.credit, entry.credit,
entry.debit, entry.debit,
entry.accountNormal entry.accountNormal
); );
const runningBalance = return calculateRunningBalance(amount, lastRunningBalance);
amount + (!isEmpty(entries) ? lastEntry.runningBalance : openingBalance); }
const newEntry = { /**
* Maps the given ledger entry to G/L transaction.
* @param {ILedgerEntry} entry
* @param {number} runningBalance
* @returns {IGeneralLedgerSheetAccountTransaction}
*/
private transactionMapper(
entry: ILedgerEntry,
runningBalance: number
): IGeneralLedgerSheetAccountTransaction {
const contact = this.repository.contactsById.get(entry.contactId);
const amount = Ledger.getAmount(
entry.credit,
entry.debit,
entry.accountNormal
);
return {
id: entry.id,
date: entry.date, date: entry.date,
dateFormatted: moment(entry.date).format('YYYY MMM DD'), dateFormatted: moment(entry.date).format('YYYY MMM DD'),
entryId: entry.id,
transactionNumber: entry.transactionNumber, transactionNumber: entry.transactionNumber,
referenceType: entry.referenceType, referenceType: entry.referenceType,
@@ -109,16 +107,15 @@ export default class GeneralLedgerSheet extends FinancialSheet {
amount, amount,
runningBalance, runningBalance,
formattedAmount: this.formatNumber(amount), formattedAmount: this.formatNumber(amount, { excerptZero: false }),
formattedCredit: this.formatNumber(entry.credit), formattedCredit: this.formatNumber(entry.credit, { excerptZero: false }),
formattedDebit: this.formatNumber(entry.debit), formattedDebit: this.formatNumber(entry.debit, { excerptZero: false }),
formattedRunningBalance: this.formatNumber(runningBalance), formattedRunningBalance: this.formatNumber(runningBalance, {
excerptZero: false,
}),
currencyCode: this.baseCurrency, currencyCode: this.baseCurrency,
}; } as IGeneralLedgerSheetAccountTransaction;
entries.push(newEntry);
return entries;
} }
/** /**
@@ -130,28 +127,48 @@ export default class GeneralLedgerSheet extends FinancialSheet {
account: IAccount, account: IAccount,
openingBalance: number openingBalance: number
): IGeneralLedgerSheetAccountTransaction[] { ): IGeneralLedgerSheetAccountTransaction[] {
const entries = this.transactions.getAccountEntries(account.id); const entries = this.repository.transactionsLedger
.whereAccountId(account.id)
.getEntries();
return entries.reduce( return entries
( .reduce((prev: Array<[number, ILedgerEntry]>, current: ILedgerEntry) => {
entries: IGeneralLedgerSheetAccountTransaction[], const prevEntry = last(prev);
entry: IJournalEntry const prevRunningBalance = head(prevEntry) as number;
) => { const amount = this.getEntryRunningBalance(
return this.entryReducer(entries, entry, openingBalance); current,
}, openingBalance,
[] prevRunningBalance
); );
return [...prev, [amount, current]];
}, [])
.map((entryPair: [number, ILedgerEntry]) => {
const [runningBalance, entry] = entryPair;
return this.transactionMapper(entry, runningBalance);
});
} }
/** /**
* Retrieve account opening balance. * Retrieves the given account opening balance.
* @param {number} accountId
* @returns {number}
*/
private accountOpeningBalance(accountId: number): number {
return this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
}
/**
* Retrieve the given account opening balance.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance} * @return {IGeneralLedgerSheetAccountBalance}
*/ */
private accountOpeningBalance( private accountOpeningBalanceTotal(
account: IAccount accountId: number
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.openingBalancesJournal.getAccountBalance(account.id); const amount = this.accountOpeningBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount); const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
const date = this.query.fromDate; const date = this.query.fromDate;
@@ -160,15 +177,31 @@ export default class GeneralLedgerSheet extends FinancialSheet {
} }
/** /**
* Retrieve account closing balance. * Retrieves the given account closing balance.
* @param {number} accountId
* @returns {number}
*/
private accountClosingBalance(accountId: number): number {
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
const transactionsBalance = this.repository.transactionsLedger
.whereAccountId(accountId)
.getClosingBalance();
return openingBalance + transactionsBalance;
}
/**
* Retrieves the given account closing balance.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccountBalance} * @return {IGeneralLedgerSheetAccountBalance}
*/ */
private accountClosingBalance( private accountClosingBalanceTotal(
openingBalance: number, accountId: number
transactions: IGeneralLedgerSheetAccountTransaction[]
): IGeneralLedgerSheetAccountBalance { ): IGeneralLedgerSheetAccountBalance {
const amount = this.calcClosingBalance(openingBalance, transactions); const amount = this.accountClosingBalance(accountId);
const formattedAmount = this.formatTotalNumber(amount); const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency; const currencyCode = this.baseCurrency;
const date = this.query.toDate; const date = this.query.toDate;
@@ -176,31 +209,78 @@ export default class GeneralLedgerSheet extends FinancialSheet {
return { amount, formattedAmount, currencyCode, date }; return { amount, formattedAmount, currencyCode, date };
} }
private calcClosingBalance( /**
openingBalance: number, * Retrieves the given account closing balance with subaccounts.
transactions: IGeneralLedgerSheetAccountTransaction[] * @param {number} accountId
) { * @returns {number}
return openingBalance + sumBy(transactions, (trans) => trans.amount); */
} private accountClosingBalanceWithSubaccounts = (
accountId: number
): number => {
const depsAccountsIds =
this.repository.accountsGraph.dependenciesOf(accountId);
const openingBalance = this.repository.openingBalanceTransactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const transactionsBalanceWithSubAccounts =
this.repository.transactionsLedger
.whereAccountsIds([...depsAccountsIds, accountId])
.getClosingBalance();
const closingBalance = openingBalance + transactionsBalanceWithSubAccounts;
return closingBalance;
};
/**
* Retrieves the closing balance with subaccounts total node.
* @param {number} accountId
* @returns {IGeneralLedgerSheetAccountBalance}
*/
private accountClosingBalanceWithSubaccountsTotal = (
accountId: number
): IGeneralLedgerSheetAccountBalance => {
const amount = this.accountClosingBalanceWithSubaccounts(accountId);
const formattedAmount = this.formatTotalNumber(amount);
const currencyCode = this.baseCurrency;
const date = this.query.toDate;
return { amount, formattedAmount, currencyCode, date };
};
/**
* Detarmines whether the closing balance subaccounts node should be exist.
* @param {number} accountId
* @returns {boolean}
*/
private isAccountNodeIncludesClosingSubaccounts = (accountId: number) => {
// Retrun early if there is no accounts in the filter so
// return closing subaccounts in all cases.
if (isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id includes transactions.
return this.repository.accountNodesIncludeTransactions.includes(accountId);
};
/** /**
* Retreive general ledger accounts sections. * Retreive general ledger accounts sections.
* @param {IAccount} account * @param {IAccount} account
* @return {IGeneralLedgerSheetAccount} * @return {IGeneralLedgerSheetAccount}
*/ */
private accountMapper(account: IAccount): IGeneralLedgerSheetAccount { private accountMapper = (account: IAccount): IGeneralLedgerSheetAccount => {
const openingBalance = this.accountOpeningBalance(account); const openingBalance = this.accountOpeningBalanceTotal(account.id);
const transactions = this.accountTransactionsMapper( const transactions = this.accountTransactionsMapper(
account, account,
openingBalance.amount openingBalance.amount
); );
const closingBalance = this.accountClosingBalance( const closingBalance = this.accountClosingBalanceTotal(account.id);
openingBalance.amount, const closingBalanceSubaccounts =
transactions this.accountClosingBalanceWithSubaccountsTotal(account.id);
);
return { const initialNode = {
id: account.id, id: account.id,
name: account.name, name: account.name,
code: account.code, code: account.code,
@@ -210,34 +290,90 @@ export default class GeneralLedgerSheet extends FinancialSheet {
transactions, transactions,
closingBalance, closingBalance,
}; };
}
return R.compose(
R.when(
() => this.isAccountNodeIncludesClosingSubaccounts(account.id),
R.assoc('closingBalanceSubaccounts', closingBalanceSubaccounts)
)
)(initialNode);
};
/** /**
* Retrieve mapped accounts with general ledger transactions and opeing/closing balance. * Maps over deep nodes to retrieve the G/L account node.
* @param {IAccount[]} accounts
* @returns {IGeneralLedgerSheetAccount[]}
*/
private accountNodesDeepMap = (
accounts: IAccount[]
): IGeneralLedgerSheetAccount[] => {
return this.mapNodesDeep(accounts, this.accountMapper);
};
/**
* Transformes the flatten nodes to nested nodes.
*/
private nestedAccountsNode = (flattenAccounts: IAccount[]): IAccount[] => {
return flatToNestedArray(flattenAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
};
/**
* Filters account nodes.
* @param {IGeneralLedgerSheetAccount[]} nodes
* @returns {IGeneralLedgerSheetAccount[]}
*/
private filterAccountNodesByTransactionsFilter = (
nodes: IGeneralLedgerSheetAccount[]
): IGeneralLedgerSheetAccount[] => {
return this.filterNodesDeep(
nodes,
(account: IGeneralLedgerSheetAccount) =>
!(account.transactions.length === 0 && this.query.noneTransactions)
);
};
/**
* Filters account nodes by the acounts filter.
* @param {IAccount[]} nodes
* @returns {IAccount[]}
*/
private filterAccountNodesByAccountsFilter = (
nodes: IAccount[]
): IAccount[] => {
return this.filterNodesDeep(nodes, (node: IGeneralLedgerSheetAccount) => {
if (R.isEmpty(this.query.accountsIds)) {
return true;
}
// Returns true if the given account id exists in the filter.
return this.repository.accountNodeInclude?.includes(node.id);
});
};
/**
* Retrieves mapped accounts with general ledger transactions and
* opeing/closing balance.
* @param {IAccount[]} accounts - * @param {IAccount[]} accounts -
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] { private accountsWalker(accounts: IAccount[]): IGeneralLedgerSheetAccount[] {
return ( return R.compose(
accounts R.defaultTo([]),
.map((account: IAccount) => this.accountMapper(account)) this.filterAccountNodesByTransactionsFilter,
// Filter general ledger accounts that have no transactions this.accountNodesDeepMap,
// when`noneTransactions` is on. R.defaultTo([]),
.filter( this.filterAccountNodesByAccountsFilter,
(generalLedgerAccount: IGeneralLedgerSheetAccount) => this.nestedAccountsNode
!( )(accounts);
generalLedgerAccount.transactions.length === 0 &&
this.query.noneTransactions
)
)
);
} }
/** /**
* Retrieve general ledger report data. * Retrieves general ledger report data.
* @return {IGeneralLedgerSheetAccount[]} * @return {IGeneralLedgerSheetAccount[]}
*/ */
public reportData(): IGeneralLedgerSheetAccount[] { public reportData(): IGeneralLedgerSheetAccount[] {
return this.accountsWalker(this.accounts); return this.accountsWalker(this.repository.accounts);
} }
} }

View File

@@ -0,0 +1,180 @@
import moment from 'moment';
import * as R from 'ramda';
import {
IAccount,
IAccountTransaction,
IContact,
IGeneralLedgerSheetQuery,
ITenant,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import { transformToMap } from '@/utils';
import { Tenant } from '@/system/models';
import { flatten, isEmpty, uniq } from 'lodash';
export class GeneralLedgerRepository {
public filter: IGeneralLedgerSheetQuery;
public accounts: IAccount[];
public transactions: IAccountTransaction[];
public openingBalanceTransactions: IAccountTransaction[];
public transactionsLedger: Ledger;
public openingBalanceTransactionsLedger: Ledger;
public repositories: any;
public models: any;
public accountsGraph: any;
public contacts: IContact;
public contactsById: Map<number, IContact>;
public tenantId: number;
public tenant: ITenant;
public accountNodesIncludeTransactions: Array<number> = [];
public accountNodeInclude: Array<number> = [];
/**
* Constructor method.
* @param models
* @param repositories
* @param filter
*/
constructor(
repositories: any,
filter: IGeneralLedgerSheetQuery,
tenantId: number
) {
this.filter = filter;
this.repositories = repositories;
this.tenantId = tenantId;
}
/**
* Initialize the G/L report.
*/
public async asyncInitialize() {
await this.initTenant();
await this.initAccounts();
await this.initAccountsGraph();
await this.initContacts();
await this.initAccountsOpeningBalance();
this.initAccountNodesIncludeTransactions();
await this.initTransactions();
this.initAccountNodesIncluded();
}
/**
* Initialize the tenant.
*/
public async initTenant() {
this.tenant = await Tenant.query()
.findById(this.tenantId)
.withGraphFetched('metadata');
}
/**
* Initialize the accounts.
*/
public async initAccounts() {
this.accounts = await this.repositories.accountRepository
.all()
.orderBy('name', 'ASC');
}
/**
* Initialize the accounts graph.
*/
public async initAccountsGraph() {
this.accountsGraph =
await this.repositories.accountRepository.getDependencyGraph();
}
/**
* Initialize the contacts.
*/
public async initContacts() {
this.contacts = await this.repositories.contactRepository.all();
this.contactsById = transformToMap(this.contacts, 'id');
}
/**
* Initialize the G/L transactions from/to the given date.
*/
public async initTransactions() {
this.transactions = await this.repositories.transactionsRepository
.journal({
fromDate: this.filter.fromDate,
toDate: this.filter.toDate,
branchesIds: this.filter.branchesIds,
})
.orderBy('date', 'ASC')
.onBuild((query) => {
if (this.filter.accountsIds?.length > 0) {
query.whereIn('accountId', this.accountNodesIncludeTransactions);
}
});
// Transform array transactions to journal collection.
this.transactionsLedger = Ledger.fromTransactions(this.transactions);
}
/**
* Initialize the G/L accounts opening balance.
*/
public async initAccountsOpeningBalance() {
// Retreive opening balance credit/debit sumation.
this.openingBalanceTransactions =
await this.repositories.transactionsRepository.journal({
toDate: moment(this.filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: this.filter.branchesIds,
});
// Accounts opening transactions.
this.openingBalanceTransactionsLedger = Ledger.fromTransactions(
this.openingBalanceTransactions
);
}
/**
* Initialize the account nodes that should include transactions.
* @returns {void}
*/
public initAccountNodesIncludeTransactions() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const childrenNodeIds = this.filter.accountsIds?.map(
(accountId: number) => {
return this.accountsGraph.dependenciesOf(accountId);
}
);
const nodeIds = R.concat(this.filter.accountsIds, childrenNodeIds);
this.accountNodesIncludeTransactions = uniq(flatten(nodeIds));
}
/**
* Initialize the account node ids should be included,
* if the filter by acounts is presented.
* @returns {void}
*/
public initAccountNodesIncluded() {
if (isEmpty(this.filter.accountsIds)) {
return;
}
const nodeIds = this.filter.accountsIds.map((accountId) => {
const childrenIds = this.accountsGraph.dependenciesOf(accountId);
const parentIds = this.accountsGraph.dependantsOf(accountId);
return R.concat(childrenIds, parentIds);
});
this.accountNodeInclude = R.compose(
R.uniq,
R.flatten,
R.concat(this.filter.accountsIds)
)(nodeIds);
}
}

View File

@@ -1,18 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from '@/exceptions';
import { difference } from 'lodash';
import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces'; import { IGeneralLedgerSheetQuery, IGeneralLedgerMeta } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import Journal from '@/services/Accounting/JournalPoster';
import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger'; import GeneralLedgerSheet from '@/services/FinancialStatements/GeneralLedger/GeneralLedger';
import { transformToMap } from 'utils';
import { Tenant } from '@/system/models';
import { GeneralLedgerMeta } from './GeneralLedgerMeta'; import { GeneralLedgerMeta } from './GeneralLedgerMeta';
import { GeneralLedgerRepository } from './GeneralLedgerRepository';
const ERRORS = {
ACCOUNTS_NOT_FOUND: 'ACCOUNTS_NOT_FOUND',
};
@Service() @Service()
export class GeneralLedgerService { export class GeneralLedgerService {
@@ -40,29 +32,13 @@ export class GeneralLedgerService {
}; };
} }
/**
* Validates accounts existance on the storage.
* @param {number} tenantId
* @param {number[]} accountsIds
*/
async validateAccountsExistance(tenantId: number, accountsIds: number[]) {
const { Account } = this.tenancy.models(tenantId);
const storedAccounts = await Account.query().whereIn('id', accountsIds);
const storedAccountsIds = storedAccounts.map((a) => a.id);
if (difference(accountsIds, storedAccountsIds).length > 0) {
throw new ServiceError(ERRORS.ACCOUNTS_NOT_FOUND);
}
}
/** /**
* Retrieve general ledger report statement. * Retrieve general ledger report statement.
* @param {number} tenantId * @param {number} tenantId
* @param {IGeneralLedgerSheetQuery} query * @param {IGeneralLedgerSheetQuery} query
* @return {IGeneralLedgerStatement} * @return {Promise<IGeneralLedgerStatement>}
*/ */
async generalLedger( public async generalLedger(
tenantId: number, tenantId: number,
query: IGeneralLedgerSheetQuery query: IGeneralLedgerSheetQuery
): Promise<{ ): Promise<{
@@ -70,60 +46,24 @@ export class GeneralLedgerService {
query: IGeneralLedgerSheetQuery; query: IGeneralLedgerSheetQuery;
meta: IGeneralLedgerMeta; meta: IGeneralLedgerMeta;
}> { }> {
const { accountRepository, transactionsRepository, contactRepository } = const repositories = this.tenancy.repositories(tenantId);
this.tenancy.repositories(tenantId);
const i18n = this.tenancy.i18n(tenantId); const i18n = this.tenancy.i18n(tenantId);
const tenant = await Tenant.query()
.findById(tenantId)
.withGraphFetched('metadata');
const filter = { const filter = {
...this.defaultQuery, ...this.defaultQuery,
...query, ...query,
}; };
// Retrieve all accounts with associated type from the storage. const genealLedgerRepository = new GeneralLedgerRepository(
const accounts = await accountRepository.all(); repositories,
const accountsGraph = await accountRepository.getDependencyGraph(); query,
tenantId
// Retrieve all contacts on the storage.
const contacts = await contactRepository.all();
const contactsByIdMap = transformToMap(contacts, 'id');
// Retreive journal transactions from/to the given date.
const transactions = await transactionsRepository.journal({
fromDate: filter.fromDate,
toDate: filter.toDate,
branchesIds: filter.branchesIds,
});
// Retreive opening balance credit/debit sumation.
const openingBalanceTrans = await transactionsRepository.journal({
toDate: moment(filter.fromDate).subtract(1, 'day'),
sumationCreditDebit: true,
branchesIds: filter.branchesIds,
});
// Transform array transactions to journal collection.
const transactionsJournal = Journal.fromTransactions(
transactions,
tenantId,
accountsGraph
);
// Accounts opening transactions.
const openingTransJournal = Journal.fromTransactions(
openingBalanceTrans,
tenantId,
accountsGraph
); );
await genealLedgerRepository.asyncInitialize();
// General ledger report instance. // General ledger report instance.
const generalLedgerInstance = new GeneralLedgerSheet( const generalLedgerInstance = new GeneralLedgerSheet(
tenantId,
filter, filter,
accounts, genealLedgerRepository,
contactsByIdMap,
transactionsJournal,
openingTransJournal,
tenant.metadata.baseCurrency,
i18n i18n
); );
// Retrieve general ledger report data. // Retrieve general ledger report data.

View File

@@ -83,8 +83,8 @@ export class GeneralLedgerTable extends R.compose(
*/ */
private openingBalanceColumnsAccessors(): IColumnMapperMeta[] { private openingBalanceColumnsAccessors(): IColumnMapperMeta[] {
return [ return [
{ key: 'date', value: this.meta.fromDate }, { key: 'date', value: 'Opening Balance' },
{ key: 'account_name', value: 'Opening Balance' }, { key: 'account_name', value: '' },
{ key: 'reference_type', accessor: '_empty_' }, { key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: 'description' }, { key: 'description', accessor: 'description' },
@@ -97,12 +97,15 @@ export class GeneralLedgerTable extends R.compose(
/** /**
* Closing balance row column accessors. * Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]} * @returns {ITableColumnAccessor[]}
*/ */
private closingBalanceColumnAccessors(): IColumnMapperMeta[] { private closingBalanceColumnAccessors(
account: IGeneralLedgerSheetAccount
): IColumnMapperMeta[] {
return [ return [
{ key: 'date', value: this.meta.toDate }, { key: 'date', value: `Closing balance for ${account.name}` },
{ key: 'account_name', value: 'Closing Balance' }, { key: 'account_name', value: `` },
{ key: 'reference_type', accessor: '_empty_' }, { key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' }, { key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' }, { key: 'description', accessor: '_empty_' },
@@ -113,6 +116,36 @@ export class GeneralLedgerTable extends R.compose(
]; ];
} }
/**
* Closing balance row column accessors.
* @param {IGeneralLedgerSheetAccount} account -
* @returns {ITableColumnAccessor[]}
*/
private closingBalanceWithSubaccountsColumnAccessors(
account: IGeneralLedgerSheetAccount
): IColumnMapperMeta[] {
return [
{
key: 'date',
value: `Closing Balance for ${account.name} with sub-accounts`,
},
{
key: 'account_name',
value: ``,
},
{ key: 'reference_type', accessor: '_empty_' },
{ key: 'reference_number', accessor: '_empty_' },
{ key: 'description', accessor: '_empty_' },
{ key: 'credit', accessor: '_empty_' },
{ key: 'debit', accessor: '_empty_' },
{ key: 'amount', accessor: 'closingBalanceSubaccounts.formattedAmount' },
{
key: 'running_balance',
accessor: 'closingBalanceSubaccounts.formattedAmount',
},
];
}
/** /**
* Retrieves the common table columns. * Retrieves the common table columns.
* @returns {ITableColumn[]} * @returns {ITableColumn[]}
@@ -184,7 +217,22 @@ export class GeneralLedgerTable extends R.compose(
* @returns {ITableRow} * @returns {ITableRow}
*/ */
private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => { private closingBalanceMapper = (account: IGeneralLedgerSheetAccount) => {
const columns = this.closingBalanceColumnAccessors(); const columns = this.closingBalanceColumnAccessors(account);
const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE],
};
return tableRowMapper(account, columns, meta);
};
/**
* Maps the given account node to opening balance table row.
* @param {IGeneralLedgerSheetAccount} account
* @returns {ITableRow}
*/
private closingBalanceWithSubaccountsMapper = (
account: IGeneralLedgerSheetAccount
): ITableRow => {
const columns = this.closingBalanceWithSubaccountsColumnAccessors(account);
const meta = { const meta = {
rowTypes: [ROW_TYPE.CLOSING_BALANCE], rowTypes: [ROW_TYPE.CLOSING_BALANCE],
}; };
@@ -221,8 +269,27 @@ export class GeneralLedgerTable extends R.compose(
rowTypes: [ROW_TYPE.ACCOUNT], rowTypes: [ROW_TYPE.ACCOUNT],
}; };
const row = tableRowMapper(account, columns, meta); const row = tableRowMapper(account, columns, meta);
const closingBalanceWithSubaccounts =
this.closingBalanceWithSubaccountsMapper(account);
return R.assoc('children', transactions)(row); // Appends the closing balance with sub-accounts row if the account
// has children accounts and the node is define.
const isAppendClosingSubaccounts = () =>
account.children?.length > 0 && !!account.closingBalanceSubaccounts;
const children = R.compose(
R.when(
isAppendClosingSubaccounts,
R.append(closingBalanceWithSubaccounts)
),
R.concat(R.defaultTo([], transactions)),
R.when(
() => account?.children?.length > 0,
R.concat(R.defaultTo([], account.children))
)
)([]);
return R.assoc('children', children)(row);
}; };
/** /**
@@ -233,7 +300,7 @@ export class GeneralLedgerTable extends R.compose(
private accountsMapper = ( private accountsMapper = (
accounts: IGeneralLedgerSheetAccount[] accounts: IGeneralLedgerSheetAccount[]
): ITableRow[] => { ): ITableRow[] => {
return this.mapNodesDeep(accounts, this.accountMapper); return this.mapNodesDeepReverse(accounts, this.accountMapper);
}; };
/** /**
@@ -250,7 +317,6 @@ export class GeneralLedgerTable extends R.compose(
*/ */
public tableColumns(): ITableColumn[] { public tableColumns(): ITableColumn[] {
const columns = this.commonColumns(); const columns = this.commonColumns();
return R.compose(this.tableColumnsCellIndexing)(columns); return R.compose(this.tableColumnsCellIndexing)(columns);
} }
} }

View File

@@ -0,0 +1,13 @@
/**
* Calculate the running balance.
* @param {number} amount - Transaction amount.
* @param {number} lastRunningBalance - Last running balance.
* @param {number} openingBalance - Opening balance.
* @return {number} Running balance.
*/
export function calculateRunningBalance(
amount: number,
lastRunningBalance: number
): number {
return amount + lastRunningBalance;
}

View File

@@ -50,8 +50,8 @@ export class JournalSheetTable extends R.compose(
{ key: 'description', accessor: 'entry.note' }, { key: 'description', accessor: 'entry.note' },
{ key: 'account_code', accessor: 'entry.accountCode' }, { key: 'account_code', accessor: 'entry.accountCode' },
{ key: 'account_name', accessor: 'entry.accountName' }, { key: 'account_name', accessor: 'entry.accountName' },
{ key: 'credit', accessor: 'entry.formattedCredit' },
{ key: 'debit', accessor: 'entry.formattedDebit' }, { key: 'debit', accessor: 'entry.formattedDebit' },
{ key: 'credit', accessor: 'entry.formattedCredit' },
]; ];
}; };
@@ -67,8 +67,8 @@ export class JournalSheetTable extends R.compose(
{ key: 'description', accessor: 'note' }, { key: 'description', accessor: 'note' },
{ key: 'account_code', accessor: 'accountCode' }, { key: 'account_code', accessor: 'accountCode' },
{ key: 'account_name', accessor: 'accountName' }, { key: 'account_name', accessor: 'accountName' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' }, { key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
]; ];
}; };
@@ -84,8 +84,8 @@ export class JournalSheetTable extends R.compose(
{ key: 'description', accessor: '_empty_' }, { key: 'description', accessor: '_empty_' },
{ key: 'account_code', accessor: '_empty_' }, { key: 'account_code', accessor: '_empty_' },
{ key: 'account_name', accessor: '_empty_' }, { key: 'account_name', accessor: '_empty_' },
{ key: 'credit', accessor: 'formattedCredit' },
{ key: 'debit', accessor: 'formattedDebit' }, { key: 'debit', accessor: 'formattedDebit' },
{ key: 'credit', accessor: 'formattedCredit' },
]; ];
}; };
@@ -101,8 +101,8 @@ export class JournalSheetTable extends R.compose(
{ key: 'description', value: '' }, { key: 'description', value: '' },
{ key: 'account_code', value: '' }, { key: 'account_code', value: '' },
{ key: 'account_name', value: '' }, { key: 'account_name', value: '' },
{ key: 'credit', value: '' },
{ key: 'debit', value: '' }, { key: 'debit', value: '' },
{ key: 'credit', value: '' },
]; ];
}; };
@@ -118,8 +118,8 @@ export class JournalSheetTable extends R.compose(
{ key: 'description', label: 'Description' }, { key: 'description', label: 'Description' },
{ key: 'account_code', label: 'Acc. Code' }, { key: 'account_code', label: 'Acc. Code' },
{ key: 'account_name', label: 'Account' }, { key: 'account_name', label: 'Account' },
{ key: 'credit', label: 'Credit' },
{ key: 'debit', label: 'Debit' }, { key: 'debit', label: 'Debit' },
{ key: 'credit', label: 'Credit' },
]; ];
} }

View File

@@ -24,6 +24,7 @@ import { ProfitLossSheetPreviousYear } from './ProfitLossSheetPreviousYear';
import { ProfitLossSheetPreviousPeriod } from './ProfitLossSheetPreviousPeriod'; import { ProfitLossSheetPreviousPeriod } from './ProfitLossSheetPreviousPeriod';
import { FinancialDateRanges } from '../FinancialDateRanges'; import { FinancialDateRanges } from '../FinancialDateRanges';
import { ProfitLossSheetFilter } from './ProfitLossSheetFilter'; import { ProfitLossSheetFilter } from './ProfitLossSheetFilter';
import { flatToNestedArray } from '@/utils';
export default class ProfitLossSheet extends R.compose( export default class ProfitLossSheet extends R.compose(
ProfitLossSheetPreviousYear, ProfitLossSheetPreviousYear,
@@ -82,14 +83,22 @@ export default class ProfitLossSheet extends R.compose(
/** /**
* Retrieve the sheet account node from the given account. * Retrieve the sheet account node from the given account.
* @param {IAccount} account * @param {IAccount} account
* @returns {IProfitLossSheetAccountNode} * @returns {IProfitLossSheetAccountNode}
*/ */
private accountNodeMapper = ( private accountNodeMapper = (
account: IAccount account: IAccount
): IProfitLossSheetAccountNode => { ): IProfitLossSheetAccountNode => {
// Retrieves the children account ids of the given account id.
const childrenAccountIds = this.repository.accountsGraph.dependenciesOf(
account.id
);
// Concat the children and the given account id.
const accountIds = R.uniq(R.append(account.id, childrenAccountIds));
// Retrieves the closing balance of the account included children accounts.
const total = this.repository.totalAccountsLedger const total = this.repository.totalAccountsLedger
.whereAccountId(account.id) .whereAccountsIds(accountIds)
.getClosingBalance(); .getClosingBalance();
return { return {
@@ -126,18 +135,19 @@ export default class ProfitLossSheet extends R.compose(
}; };
/** /**
* Retrieve report accounts nodes by the given accounts types. * Retrieves report accounts nodes by the given accounts types.
* @param {string[]} types * @param {string[]} types
* @returns {IBalanceSheetAccountNode} * @returns {IBalanceSheetAccountNode}
*/ */
private getAccountsNodesByTypes = ( private getAccountsNodesByTypes = (
types: string[] types: string[]
): IProfitLossSheetAccountNode[] => { ): IProfitLossSheetAccountNode[] => {
return R.compose( const accounts = this.repository.getAccountsByType(types);
R.map(this.accountNodeCompose), const accountsTree = flatToNestedArray(accounts, {
R.flatten, id: 'id',
R.map(this.repository.getAccountsByType) parentId: 'parentAccountId',
)(types); });
return this.mapNodesDeep(accountsTree, this.accountNodeCompose);
}; };
/** /**

View File

@@ -1,4 +1,4 @@
import { defaultTo } from 'lodash'; import { castArray, defaultTo } from 'lodash';
import * as R from 'ramda'; import * as R from 'ramda';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@@ -31,6 +31,11 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
*/ */
public accounts: IAccount[]; public accounts: IAccount[];
/**
*
*/
public accountsGraph: any;
/** /**
* Transactions group type. * Transactions group type.
* @param {IAccountTransactionsGroupBy} * @param {IAccountTransactionsGroupBy}
@@ -135,6 +140,8 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
*/ */
public asyncInitialize = async () => { public asyncInitialize = async () => {
await this.initAccounts(); await this.initAccounts();
await this.initAccountsGraph();
await this.initAccountsTotalLedger(); await this.initAccountsTotalLedger();
// Date Periods. // Date Periods.
@@ -177,6 +184,15 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
this.accountsByType = transformToMapBy(accounts, 'accountType'); this.accountsByType = transformToMapBy(accounts, 'accountType');
}; };
/**
* Initialize accounts graph.
*/
private initAccountsGraph = async () => {
const { Account } = this.models;
this.accountsGraph = Account.toDependencyGraph(this.accounts);
};
// ---------------------------- // ----------------------------
// # Closing Total. // # Closing Total.
// ---------------------------- // ----------------------------
@@ -337,7 +353,18 @@ export class ProfitLossSheetRepository extends R.compose(FinancialDatePeriods)(
return Account.query(); return Account.query();
}; };
public getAccountsByType = (type: string) => { /**
return defaultTo(this.accountsByType.get(type), []); *
* @param type
* @returns
*/
public getAccountsByType = (type: string[] | string) => {
return R.compose(
R.flatten,
R.map((accountType) =>
R.defaultTo([], this.accountsByType.get(accountType))
),
castArray
)(type);
}; };
} }

View File

@@ -46,6 +46,9 @@ export default class SyncSystemSendInvite {
email: user.email, email: user.email,
active: user.active, active: user.active,
tenantId, tenantId,
// Email should be verified since the user got the invite token through email.
verified: true,
}); });
// Creates a invite user token. // Creates a invite user token.
const invite = await Invite.query().insert({ const invite = await Invite.query().insert({

View File

@@ -13,6 +13,7 @@ export class ManualJournalTransfromer extends Transformer {
'formattedAmount', 'formattedAmount',
'formattedDate', 'formattedDate',
'formattedPublishedAt', 'formattedPublishedAt',
'formattedCreatedAt',
'attachments', 'attachments',
]; ];
}; };
@@ -37,6 +38,15 @@ export class ManualJournalTransfromer extends Transformer {
return this.formatDate(manualJorunal.date); return this.formatDate(manualJorunal.date);
}; };
/**
* Retrieve formatted created at date.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedCreatedAt = (manualJorunal: IManualJournal): string => {
return this.formatDate(manualJorunal.createdAt);
};
/** /**
* Retrieve formatted published at date. * Retrieve formatted published at date.
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal

View File

@@ -207,7 +207,7 @@ export default class OrganizationService {
): IOrganizationBuildDTO { ): IOrganizationBuildDTO {
return { return {
...buildDTO, ...buildDTO,
dateFormat: defaultTo(buildDTO.dateFormat, 'DD/MM/yyyy'), dateFormat: defaultTo(buildDTO.dateFormat, 'DD MMM yyyy'),
}; };
} }

View File

@@ -1,5 +1,6 @@
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from '@/utils'; import { formatNumber } from '@/utils';
import { PurchaseInvoiceTransformer } from '../Bills/PurchaseInvoiceTransformer';
export class BillPaymentEntryTransformer extends Transformer { export class BillPaymentEntryTransformer extends Transformer {
/** /**
@@ -7,7 +8,14 @@ export class BillPaymentEntryTransformer extends Transformer {
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['paymentAmountFormatted']; return ['paymentAmountFormatted', 'bill'];
};
/**
* Retreives the
*/
protected bill = (entry) => {
return this.item(entry.bill, new PurchaseInvoiceTransformer());
}; };
/** /**

View File

@@ -12,6 +12,7 @@ export class BillPaymentTransformer extends Transformer {
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'entries', 'entries',
'attachments', 'attachments',
@@ -27,6 +28,15 @@ export class BillPaymentTransformer extends Transformer {
return this.formatDate(billPayment.paymentDate); return this.formatDate(billPayment.paymentDate);
}; };
/**
* Retrieve formatted created at date.
* @param {IBillPayment} billPayment
* @returns {string}
*/
protected formattedCreatedAt = (billPayment: IBillPayment): string => {
return this.formatDate(billPayment.createdAt);
}
/** /**
* Retrieve formatted bill amount. * Retrieve formatted bill amount.
* @param {IBill} invoice * @param {IBill} invoice

View File

@@ -14,6 +14,7 @@ export class PurchaseInvoiceTransformer extends Transformer {
return [ return [
'formattedBillDate', 'formattedBillDate',
'formattedDueDate', 'formattedDueDate',
'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'formattedPaymentAmount', 'formattedPaymentAmount',
'formattedBalance', 'formattedBalance',
@@ -57,6 +58,15 @@ export class PurchaseInvoiceTransformer extends Transformer {
return this.formatDate(bill.dueDate); return this.formatDate(bill.dueDate);
}; };
/**
* Retrieve the formatted created at date.
* @param {IBill} bill
* @returns {string}
*/
protected formattedCreatedAt = (bill: IBill): string => {
return this.formatDate(bill.createdAt);
};
/** /**
* Retrieve formatted bill amount. * Retrieve formatted bill amount.
* @param {IBill} bill * @param {IBill} bill

View File

@@ -14,6 +14,7 @@ export class VendorCreditTransformer extends Transformer {
'formattedAmount', 'formattedAmount',
'formattedSubtotal', 'formattedSubtotal',
'formattedVendorCreditDate', 'formattedVendorCreditDate',
'formattedCreatedAt',
'formattedCreditsRemaining', 'formattedCreditsRemaining',
'formattedInvoicedAmount', 'formattedInvoicedAmount',
'entries', 'entries',
@@ -30,6 +31,15 @@ export class VendorCreditTransformer extends Transformer {
return this.formatDate(vendorCredit.vendorCreditDate); return this.formatDate(vendorCredit.vendorCreditDate);
}; };
/**
* Retireve formatted created at date.
* @param vendorCredit
* @returns {string}
*/
protected formattedCreatedAt = (vendorCredit): string => {
return this.formatDate(vendorCredit.createdAt);
};
/** /**
* Retrieve formatted vendor credit amount. * Retrieve formatted vendor credit amount.
* @param {IVendorCredit} credit * @param {IVendorCredit} credit

View File

@@ -108,17 +108,28 @@ export default class ResourceService {
const $hasFields = (field) => const $hasFields = (field) =>
'undefined' !== typeof field.fields ? field : undefined; 'undefined' !== typeof field.fields ? field : undefined;
const $hasColumns = (column) => const $ColumnHasColumns = (column) =>
'undefined' !== typeof column.columns ? column : undefined; 'undefined' !== typeof column.columns ? column : undefined;
const $hasColumns = (columns) =>
'undefined' !== typeof columns ? columns : undefined;
const naviagations = [ const naviagations = [
['fields', qim.$each, 'name'], ['fields', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, 'name'], ['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'], ['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'], ['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
['columns', qim.$each, 'name'], ['columns', $hasColumns, qim.$each, 'name'],
['columns', qim.$each, $hasColumns, 'columns', qim.$each, 'name'], [
'columns',
$hasColumns,
qim.$each,
$ColumnHasColumns,
'columns',
qim.$each,
'name',
],
]; ];
return this.i18nService.i18nApply(naviagations, meta, tenantId); return this.i18nService.i18nApply(naviagations, meta, tenantId);
} }

View File

@@ -18,6 +18,7 @@ export class SaleEstimateTransfromer extends Transformer {
'formattedDeliveredAtDate', 'formattedDeliveredAtDate',
'formattedApprovedAtDate', 'formattedApprovedAtDate',
'formattedRejectedAtDate', 'formattedRejectedAtDate',
'formattedCreatedAt',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -41,6 +42,15 @@ export class SaleEstimateTransfromer extends Transformer {
return this.formatDate(estimate.expirationDate); return this.formatDate(estimate.expirationDate);
}; };
/**
* Retrieves the formatted estimate created at.
* @param {ISaleEstimate} estimate -
* @returns {string}
*/
protected formattedCreatedAt = (estimate: ISaleEstimate): string => {
return this.formatDate(estimate.createdAt);
};
/** /**
* Retrieve formatted estimate date. * Retrieve formatted estimate date.
* @param {ISaleEstimate} invoice * @param {ISaleEstimate} invoice

View File

@@ -13,6 +13,7 @@ export class SaleInvoiceTransformer extends Transformer {
return [ return [
'invoiceDateFormatted', 'invoiceDateFormatted',
'dueDateFormatted', 'dueDateFormatted',
'createdAtFormatted',
'dueAmountFormatted', 'dueAmountFormatted',
'paymentAmountFormatted', 'paymentAmountFormatted',
'balanceAmountFormatted', 'balanceAmountFormatted',
@@ -48,6 +49,15 @@ export class SaleInvoiceTransformer extends Transformer {
return this.formatDate(invoice.dueDate); return this.formatDate(invoice.dueDate);
}; };
/**
* Retrieve the formatted created at date.
* @param invoice
* @returns {string}
*/
protected createdAtFormatted = (invoice): string => {
return this.formatDate(invoice.createdAt);
};
/** /**
* Retrieve formatted invoice due amount. * Retrieve formatted invoice due amount.
* @param {ISaleInvoice} invoice * @param {ISaleInvoice} invoice

View File

@@ -146,6 +146,7 @@ export class EditPaymentReceive {
paymentReceiveId, paymentReceiveId,
paymentReceive, paymentReceive,
oldPaymentReceive, oldPaymentReceive,
paymentReceiveDTO,
authorizedUser, authorizedUser,
trx, trx,
} as IPaymentReceiveEditedPayload); } as IPaymentReceiveEditedPayload);

View File

@@ -12,6 +12,7 @@ export class PaymentReceiveTransfromer extends Transformer {
return [ return [
'subtotalFormatted', 'subtotalFormatted',
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'formattedExchangeRate', 'formattedExchangeRate',
'entries', 'entries',
@@ -27,9 +28,18 @@ export class PaymentReceiveTransfromer extends Transformer {
return this.formatDate(payment.paymentDate); return this.formatDate(payment.paymentDate);
}; };
/**
* Retrieves the formatted created at date.
* @param {IPaymentReceive} payment
* @returns {string}
*/
protected formattedCreatedAt = (payment: IPaymentReceive): string => {
return this.formatDate(payment.createdAt);
};
/** /**
* Retrieve the formatted payment subtotal. * Retrieve the formatted payment subtotal.
* @param {IPaymentReceive} payment * @param {IPaymentReceive} payment
* @returns {string} * @returns {string}
*/ */
protected subtotalFormatted = (payment: IPaymentReceive): string => { protected subtotalFormatted = (payment: IPaymentReceive): string => {

View File

@@ -17,6 +17,7 @@ export class SaleReceiptTransformer extends Transformer {
'formattedAmount', 'formattedAmount',
'formattedReceiptDate', 'formattedReceiptDate',
'formattedClosedAtDate', 'formattedClosedAtDate',
'formattedCreatedAt',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -40,6 +41,15 @@ export class SaleReceiptTransformer extends Transformer {
return this.formatDate(receipt.closedAt); return this.formatDate(receipt.closedAt);
}; };
/**
* Retrieve formatted receipt created at date.
* @param receipt
* @returns {string}
*/
protected formattedCreatedAt = (receipt: ISaleReceipt): string => {
return this.formatDate(receipt.createdAt);
};
/** /**
* Retrieves the estimate formatted subtotal. * Retrieves the estimate formatted subtotal.
* @param {ISaleReceipt} receipt * @param {ISaleReceipt} receipt

View File

@@ -90,6 +90,20 @@ export default class SystemUser extends SystemModel {
}; };
} }
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Filters the invite accepted users.
*/
inviteAccepted(query) {
query.whereNotNull('invite_accepted_at');
},
};
}
/** /**
* Verify the password of the user. * Verify the password of the user.
* @param {String} password - The given password. * @param {String} password - The given password.

View File

@@ -0,0 +1,18 @@
// @ts-nocheck
import { FSuggest } from '../Forms';
interface BranchSuggestFieldProps {
items: any[];
}
export function BranchSuggestField({ ...props }: BranchSuggestFieldProps) {
return (
<FSuggest
valueAccessor={'id'}
labelAccessor={'code'}
textAccessor={'name'}
inputProps={{ placeholder: 'Select a branch' }}
{...props}
/>
);
}

View File

@@ -21,9 +21,9 @@ import RefundCreditNoteDetailDrawer from '@/containers/Drawers/RefundCreditNoteD
import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCreditDetailDrawer'; import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCreditDetailDrawer';
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer'; import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer'; import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import CategorizeTransactionDrawer from '@/containers/CashFlow/CategorizeTransaction/drawers/CategorizeTransactionDrawer/CategorizeTransactionDrawer';
/** /**
* Drawers container of the dashboard. * Drawers container of the dashboard.

View File

@@ -16,8 +16,7 @@ export const useManualJournalsColumns = () => {
{ {
id: 'date', id: 'date',
Header: intl.get('date'), Header: intl.get('date'),
accessor: 'date', accessor: 'formatted_date',
Cell: FormatDateCell,
width: 115, width: 115,
className: 'date', className: 'date',
clickable: true, clickable: true,
@@ -66,8 +65,7 @@ export const useManualJournalsColumns = () => {
{ {
id: 'created_at', id: 'created_at',
Header: intl.get('created_at'), Header: intl.get('created_at'),
accessor: 'created_at', accessor: 'formatted_created_at',
Cell: FormatDateCell,
width: 125, width: 125,
clickable: true, clickable: true,
}, },

View File

@@ -13,7 +13,7 @@ import {
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import classNames from 'classnames'; import classNames from 'classnames';
import { Icon, If, FormattedMessage as T } from '@/components'; import { Group, Icon, If, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useMakeJournalFormContext } from './MakeJournalProvider'; import { useMakeJournalFormContext } from './MakeJournalProvider';
@@ -76,7 +76,10 @@ export default function MakeJournalFloatingAction() {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Publish ----------- */} {/* ----------- Save And Publish ----------- */}
<If condition={!manualJournal || !manualJournal?.is_published}> <If condition={!manualJournal || !manualJournal?.is_published}>
<ButtonGroup> <ButtonGroup>
@@ -188,6 +191,6 @@ export default function MakeJournalFloatingAction() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -111,16 +111,16 @@ export const useJournalTableEntriesColumns = () => {
fieldProps: { allowCreate: true }, fieldProps: { allowCreate: true },
}, },
{ {
Header: CreditHeaderCell, Header: DebitHeaderCell,
accessor: 'credit', accessor: 'debit',
Cell: MoneyFieldCell, Cell: MoneyFieldCell,
disableSortBy: true, disableSortBy: true,
width: 100, width: 100,
align: Align.Right, align: Align.Right,
}, },
{ {
Header: DebitHeaderCell, Header: CreditHeaderCell,
accessor: 'debit', accessor: 'credit',
Cell: MoneyFieldCell, Cell: MoneyFieldCell,
disableSortBy: true, disableSortBy: true,
width: 100, width: 100,

View File

@@ -1,5 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React, { useMemo } from 'react';
import { first } from 'lodash';
import { DrawerHeaderContent, DrawerLoading } from '@/components'; import { DrawerHeaderContent, DrawerLoading } from '@/components';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
import { import {
@@ -34,6 +35,12 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
isLoading: isUncategorizedTransactionLoading, isLoading: isUncategorizedTransactionLoading,
} = useUncategorizedTransaction(uncategorizedTransactionId); } = useUncategorizedTransaction(uncategorizedTransactionId);
// Retrieves the primary branch.
const primaryBranch = useMemo(
() => branches?.find((b) => b.primary) || first(branches),
[branches],
);
const provider = { const provider = {
uncategorizedTransactionId, uncategorizedTransactionId,
uncategorizedTransaction, uncategorizedTransaction,
@@ -42,6 +49,7 @@ function CategorizeTransactionBoot({ uncategorizedTransactionId, ...props }) {
accounts, accounts,
isBranchesLoading, isBranchesLoading,
isAccountsLoading, isAccountsLoading,
primaryBranch,
}; };
const isLoading = const isLoading =
isBranchesLoading || isUncategorizedTransactionLoading || isAccountsLoading; isBranchesLoading || isUncategorizedTransactionLoading || isAccountsLoading;

View File

@@ -0,0 +1,22 @@
// @ts-nocheck
import { FFormGroup, FeatureCan } from '@/components';
import { useCategorizeTransactionBoot } from './CategorizeTransactionBoot';
import { Features } from '@/constants';
import { BranchSuggestField } from '@/components/Branches/BranchSuggestField_';
export function CategorizeTransactionBranchField() {
const { branches } = useCategorizeTransactionBoot();
return (
<FFormGroup name={'branchId'} label={'Branch'} fastField inline>
<FeatureCan feature={Features.Branches}>
<BranchSuggestField
name={'branchId'}
items={branches}
popoverProps={{ minimal: true }}
fill
/>
</FeatureCan>
</FFormGroup>
);
}

View File

@@ -24,8 +24,11 @@ function CategorizeTransactionFormRoot({
// #withDrawerActions // #withDrawerActions
closeDrawer, closeDrawer,
}) { }) {
const { uncategorizedTransactionId, uncategorizedTransaction } = const {
useCategorizeTransactionBoot(); uncategorizedTransactionId,
uncategorizedTransaction,
primaryBranch,
} = useCategorizeTransactionBoot();
const { mutateAsync: categorizeTransaction } = useCategorizeTransaction(); const { mutateAsync: categorizeTransaction } = useCategorizeTransaction();
// Callbacks handles form submit. // Callbacks handles form submit.
@@ -37,18 +40,28 @@ function CategorizeTransactionFormRoot({
.then(() => { .then(() => {
setSubmitting(false); setSubmitting(false);
closeDrawer(DRAWERS.CATEGORIZE_TRANSACTION); closeDrawer(DRAWERS.CATEGORIZE_TRANSACTION);
AppToaster.show({ AppToaster.show({
message: 'The uncategorized transaction has been categorized.', message: 'The uncategorized transaction has been categorized.',
intent: Intent.SUCCESS, intent: Intent.SUCCESS,
}); });
}) })
.catch(() => { .catch((err) => {
setSubmitting(false); setSubmitting(false);
AppToaster.show({ if (
message: 'Something went wrong!', err.response.data?.errors?.some(
intent: Intent.DANGER, (e) => e.type === 'BRANCH_ID_REQUIRED',
}); )
) {
setErrors({
branchId: 'The branch is required.',
});
} else {
AppToaster.show({
message: 'Something went wrong!',
intent: Intent.DANGER,
});
}
}); });
}; };
// Form initial values in create and edit mode. // Form initial values in create and edit mode.
@@ -60,6 +73,9 @@ function CategorizeTransactionFormRoot({
* as well. * as well.
*/ */
...transformToCategorizeForm(uncategorizedTransaction), ...transformToCategorizeForm(uncategorizedTransaction),
/** Assign the primary branch id as default value. */
branchId: primaryBranch?.id || null,
}; };
return ( return (

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherIncome() { export default function CategorizeTransactionOtherIncome() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOtherIncome() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerContribution() { export default function CategorizeTransactionOwnerContribution() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -63,6 +64,8 @@ export default function CategorizeTransactionOwnerContribution() {
<FFormGroup name={'description'} label={'Description'} fastField inline> <FFormGroup name={'description'} label={'Description'} fastField inline>
<FTextArea name={'description'} growVertically large fill /> <FTextArea name={'description'} growVertically large fill />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionTransferFrom() { export default function CategorizeTransactionTransferFrom() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -47,7 +48,7 @@ export default function CategorizeTransactionTransferFrom() {
inline inline
> >
<AccountsSelect <AccountsSelect
name={'to_account_id'} name={'creditAccountId'}
items={accounts} items={accounts}
filterByRootTypes={['asset']} filterByRootTypes={['asset']}
fastField fastField
@@ -68,6 +69,8 @@ export default function CategorizeTransactionTransferFrom() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOtherExpense() { export default function CategorizeTransactionOtherExpense() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOtherExpense() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionOwnerDrawings() { export default function CategorizeTransactionOwnerDrawings() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -68,6 +69,8 @@ export default function CategorizeTransactionOwnerDrawings() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -8,6 +8,7 @@ import {
FTextArea, FTextArea,
} from '@/components'; } from '@/components';
import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot'; import { useCategorizeTransactionBoot } from '../CategorizeTransactionBoot';
import { CategorizeTransactionBranchField } from '../CategorizeTransactionBranchField';
export default function CategorizeTransactionToAccount() { export default function CategorizeTransactionToAccount() {
const { accounts } = useCategorizeTransactionBoot(); const { accounts } = useCategorizeTransactionBoot();
@@ -49,7 +50,7 @@ export default function CategorizeTransactionToAccount() {
<AccountsSelect <AccountsSelect
name={'creditAccountId'} name={'creditAccountId'}
items={accounts} items={accounts}
filterByRootTypes={['assset']} filterByRootTypes={['asset']}
fastField fastField
fill fill
allowCreate allowCreate
@@ -68,6 +69,8 @@ export default function CategorizeTransactionToAccount() {
fill={true} fill={true}
/> />
</FFormGroup> </FFormGroup>
<CategorizeTransactionBranchField />
</> </>
); );
} }

View File

@@ -11,6 +11,7 @@ export const defaultInitialValues = {
transactionType: '', transactionType: '',
referenceNo: '', referenceNo: '',
description: '', description: '',
branchId: '',
}; };
export const transformToCategorizeForm = (uncategorizedTransaction) => { export const transformToCategorizeForm = (uncategorizedTransaction) => {

View File

@@ -13,12 +13,11 @@ import {
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { Icon, FormattedMessage as T } from '@/components'; import { Group, Icon, FormattedMessage as T } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useCustomerFormContext } from './CustomerFormProvider'; import { useCustomerFormContext } from './CustomerFormProvider';
import { safeInvoke } from '@/utils'; import { safeInvoke } from '@/utils';
/** /**
* Customer floating actions bar. * Customer floating actions bar.
*/ */
@@ -51,7 +50,10 @@ export default function CustomerFloatingActions({ onCancel }) {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
<ButtonGroup> <ButtonGroup>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<SaveButton <SaveButton
@@ -96,7 +98,7 @@ export default function CustomerFloatingActions({ onCancel }) {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -26,6 +26,14 @@ export const useAccountReadEntriesColumns = () => {
width: 100, width: 100,
textOverview: true, textOverview: true,
}, },
{
Header: intl.get('debit'),
accessor: isFCYCurrencyType ? 'formatted_fc_debit' : 'formatted_debit',
width: 80,
className: 'debit',
align: 'right',
textOverview: true,
},
{ {
Header: intl.get('credit'), Header: intl.get('credit'),
accessor: isFCYCurrencyType accessor: isFCYCurrencyType
@@ -36,14 +44,6 @@ export const useAccountReadEntriesColumns = () => {
align: 'right', align: 'right',
textOverview: true, textOverview: true,
}, },
{
Header: intl.get('debit'),
accessor: isFCYCurrencyType ? 'formatted_fc_debit' : 'formatted_debit',
width: 80,
className: 'debit',
align: 'right',
textOverview: true,
},
], ],
[isFCYCurrencyType], [isFCYCurrencyType],
); );

View File

@@ -41,19 +41,23 @@ export default function BillDetailHeader() {
<Col xs={6}> <Col xs={6}>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem label={intl.get('bill_date')}> <DetailItem label={intl.get('bill_date')}>
<FormatDate value={bill.bill_date} /> {bill.formatted_bill_date}
</DetailItem> </DetailItem>
<DetailItem label={intl.get('due_date')}> <DetailItem label={intl.get('due_date')}>
<FormatDate value={bill.due_date} /> {bill.formatted_due_date}
</DetailItem> </DetailItem>
<DetailItem label={intl.get('vendor_name')}> <DetailItem label={intl.get('vendor_name')}>
<VendorDrawerLink vendorId={bill.vendor_id}> <VendorDrawerLink vendorId={bill.vendor_id}>
{bill.vendor?.display_name} {bill.vendor?.display_name}
</VendorDrawerLink> </VendorDrawerLink>
</DetailItem> </DetailItem>
<DetailItem label={intl.get('bill.details.bill_number')}> <DetailItem label={intl.get('bill.details.bill_number')}>
{defaultTo(bill.bill_number, '-')} {defaultTo(bill.bill_number, '-')}
</DetailItem> </DetailItem>
<ExchangeRateDetailItem <ExchangeRateDetailItem
exchangeRate={bill?.exchange_rate} exchangeRate={bill?.exchange_rate}
toCurrency={bill?.currency_code} toCurrency={bill?.currency_code}
@@ -75,7 +79,7 @@ export default function BillDetailHeader() {
/> />
<DetailItem <DetailItem
label={intl.get('bill.details.created_at')} label={intl.get('bill.details.created_at')}
children={<FormatDate value={bill.created_at} />} children={bill.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -46,7 +46,7 @@ export default function CashflowTransactionDrawerHeader() {
</DetailItem> </DetailItem>
<DetailItem label={<T id={'date'} />}> <DetailItem label={<T id={'date'} />}>
<FormatDate value={cashflowTransaction.date} /> {cashflowTransaction.formatted_date}
</DetailItem> </DetailItem>
<DetailItem name={'reference-no'} label={<T id={'reference_no'} />}> <DetailItem name={'reference-no'} label={<T id={'reference_no'} />}>

View File

@@ -5,13 +5,11 @@ import styled from 'styled-components';
import { defaultTo } from 'lodash'; import { defaultTo } from 'lodash';
import { import {
FormatDate,
T, T,
Row, Row,
Col, Col,
DetailsMenu, DetailsMenu,
DetailItem, DetailItem,
ButtonLink,
CommercialDocHeader, CommercialDocHeader,
CommercialDocTopHeader, CommercialDocTopHeader,
CustomerDrawerLink, CustomerDrawerLink,
@@ -47,7 +45,7 @@ export default function CreditNoteDetailHeader() {
<DetailItem <DetailItem
label={intl.get('credit_note.drawer.label_credit_note_date')} label={intl.get('credit_note.drawer.label_credit_note_date')}
> >
<FormatDate value={creditNote.formatted_credit_note_date} /> {creditNote.formatted_credit_note_date}
</DetailItem> </DetailItem>
<DetailItem <DetailItem
@@ -85,7 +83,7 @@ export default function CreditNoteDetailHeader() {
/> />
<DetailItem <DetailItem
label={<T id={'credit_note.drawer.label_created_at'} />} label={<T id={'credit_note.drawer.label_created_at'} />}
children={<FormatDate value={creditNote.created_at} />} children={creditNote.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -81,7 +81,7 @@ export default function EstimateDetailHeader() {
/> />
<DetailItem <DetailItem
label={<T id={'estimate.details.created_at'} />} label={<T id={'estimate.details.created_at'} />}
children={<FormatDate value={estimate.created_at} />} children={estimate.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -1,6 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import moment from 'moment';
import styled from 'styled-components'; import styled from 'styled-components';
import { defaultTo } from 'lodash'; import { defaultTo } from 'lodash';
@@ -42,7 +41,7 @@ export default function ExpenseDrawerHeader() {
<Col xs={6}> <Col xs={6}>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem name={'date'} label={<T id={'date'} />}> <DetailItem name={'date'} label={<T id={'date'} />}>
{moment(expense.payment_date).format('YYYY MMM DD')} {expense.formatted_payment_date}
</DetailItem> </DetailItem>
<DetailItem name={'reference'} label={<T id={'reference_no'} />}> <DetailItem name={'reference'} label={<T id={'reference_no'} />}>
@@ -66,11 +65,11 @@ export default function ExpenseDrawerHeader() {
minLabelSize={'180px'} minLabelSize={'180px'}
> >
<DetailItem label={<T id={'published_at'} />}> <DetailItem label={<T id={'published_at'} />}>
<FormatDate value={expense.published_at} /> {expense.formatted_date}
</DetailItem> </DetailItem>
<DetailItem label={<T id={'created_at'} />}> <DetailItem label={<T id={'created_at'} />}>
<FormatDate value={expense.created_at} /> {expense.formatted_created_at}
</DetailItem> </DetailItem>
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -43,11 +43,11 @@ export default function InvoiceDetailHeader() {
<Col xs={6}> <Col xs={6}>
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem label={intl.get('invoice_date')}> <DetailItem label={intl.get('invoice_date')}>
<FormatDate value={invoice.invoice_date} /> {invoice.invoice_date_formatted}
</DetailItem> </DetailItem>
<DetailItem label={intl.get('due_date')}> <DetailItem label={intl.get('due_date')}>
<FormatDate value={invoice.due_date} /> {invoice.due_date_formatted}
</DetailItem> </DetailItem>
<DetailItem label={intl.get('customer_name')}> <DetailItem label={intl.get('customer_name')}>
@@ -86,7 +86,7 @@ export default function InvoiceDetailHeader() {
/> />
<DetailItem <DetailItem
label={intl.get('invoice.details.created_at')} label={intl.get('invoice.details.created_at')}
children={<FormatDate value={invoice.created_at} />} children={invoice.created_at_formatted}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -82,20 +82,6 @@ export const useManualJournalEntriesColumns = () => {
}, },
] ]
: []), : []),
{
Header: intl.get('credit'),
accessor: 'credit',
Cell: FormatNumberCell,
width: getColumnWidth(entries, 'credit', {
minWidth: 60,
magicSpacing: 5,
}),
disableResizable: true,
disableSortBy: true,
textOverview: true,
formatNumber: { noZero: true },
align: 'right',
},
{ {
Header: intl.get('debit'), Header: intl.get('debit'),
accessor: 'debit', accessor: 'debit',
@@ -110,6 +96,20 @@ export const useManualJournalEntriesColumns = () => {
formatNumber: { noZero: true }, formatNumber: { noZero: true },
align: 'right', align: 'right',
}, },
{
Header: intl.get('credit'),
accessor: 'credit',
Cell: FormatNumberCell,
width: getColumnWidth(entries, 'credit', {
minWidth: 60,
magicSpacing: 5,
}),
disableResizable: true,
disableSortBy: true,
textOverview: true,
formatNumber: { noZero: true },
align: 'right',
},
], ],
[], [],
); );

View File

@@ -37,7 +37,7 @@ export default function PaymentMadeDetailHeader() {
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem <DetailItem
label={intl.get('payment_date')} label={intl.get('payment_date')}
children={<FormatDate value={paymentMade.payment_date} />} children={paymentMade.formatted_payment_date}
/> />
<DetailItem <DetailItem
label={intl.get('payment_made.details.payment_number')} label={intl.get('payment_made.details.payment_number')}
@@ -58,6 +58,7 @@ export default function PaymentMadeDetailHeader() {
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>
<Col xs={6}> <Col xs={6}>
<DetailsMenu <DetailsMenu
textAlign={'right'} textAlign={'right'}
@@ -70,7 +71,7 @@ export default function PaymentMadeDetailHeader() {
/> />
<DetailItem <DetailItem
label={intl.get('created_at')} label={intl.get('created_at')}
children={<FormatDate value={paymentMade.created_at} />} children={paymentMade.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -1,8 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import moment from 'moment';
import { getColumnWidth } from '@/utils'; import { getColumnWidth } from '@/utils';
import { FormatNumberCell } from '@/components'; import { FormatNumberCell } from '@/components';
import { usePaymentMadeDetailContext } from './PaymentMadeDetailProvider'; import { usePaymentMadeDetailContext } from './PaymentMadeDetailProvider';
@@ -17,7 +15,7 @@ export const usePaymentMadeEntriesColumns = () => {
() => [ () => [
{ {
Header: intl.get('date'), Header: intl.get('date'),
accessor: (row) => moment(row.date).format('YYYY MMM DD'), accessor: 'bill.formatted_bill_date',
width: 100, width: 100,
disableSortBy: true, disableSortBy: true,
className: 'date', className: 'date',

View File

@@ -5,7 +5,6 @@ import { defaultTo } from 'lodash';
import { import {
Row, Row,
Col, Col,
FormatDate,
DetailsMenu, DetailsMenu,
DetailItem, DetailItem,
CommercialDocHeader, CommercialDocHeader,
@@ -36,7 +35,7 @@ export default function PaymentReceiveDetailHeader() {
<DetailsMenu direction={'horizantal'} minLabelSize={'180px'}> <DetailsMenu direction={'horizantal'} minLabelSize={'180px'}>
<DetailItem <DetailItem
label={intl.get('payment_date')} label={intl.get('payment_date')}
children={<FormatDate value={paymentReceive.payment_date} />} children={paymentReceive.formatted_payment_date}
/> />
<DetailItem <DetailItem
label={intl.get('payment_receive.details.payment_number')} label={intl.get('payment_receive.details.payment_number')}
@@ -71,7 +70,7 @@ export default function PaymentReceiveDetailHeader() {
/> />
<DetailItem <DetailItem
label={intl.get('created_at')} label={intl.get('created_at')}
children={<FormatDate value={paymentReceive.created_at} />} children={paymentReceive.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -1,7 +1,6 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import moment from 'moment';
import { import {
Button, Button,
Popover, Popover,
@@ -26,7 +25,7 @@ export const usePaymentReceiveEntriesColumns = () => {
() => [ () => [
{ {
Header: intl.get('date'), Header: intl.get('date'),
accessor: (row) => moment(row.payment_date).format('YYYY MMM DD'), accessor: 'invoice.invoice_date_formatted',
width: 100, width: 100,
className: 'date', className: 'date',
disableSortBy: true, disableSortBy: true,

View File

@@ -54,11 +54,11 @@ export default function ReceiptDetailHeader() {
</DetailItem> </DetailItem>
<DetailItem <DetailItem
label={intl.get('receipt_date')} label={intl.get('receipt_date')}
children={<FormatDate value={receipt.receipt_date} />} children={receipt.formatted_receipt_date}
/> />
<DetailItem <DetailItem
label={intl.get('closed_date')} label={intl.get('closed_date')}
children={<FormatDate value={receipt.closed_at_date} />} children={receipt.formatted_closed_at_date}
/> />
<ExchangeRateDetailItem <ExchangeRateDetailItem
exchangeRate={receipt?.exchange_rate} exchangeRate={receipt?.exchange_rate}
@@ -82,7 +82,7 @@ export default function ReceiptDetailHeader() {
/> />
<DetailItem <DetailItem
label={intl.get('receipt.details.created_at')} label={intl.get('receipt.details.created_at')}
children={<FormatDate value={receipt.created_at} />} children={receipt.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -42,7 +42,7 @@ export default function VendorCreditDetailHeader() {
<DetailItem <DetailItem
label={intl.get('vendor_credit.drawer.label_vendor_credit_date')} label={intl.get('vendor_credit.drawer.label_vendor_credit_date')}
> >
<FormatDate value={vendorCredit.formatted_vendor_credit_date} /> {vendorCredit.formatted_vendor_credit_date}
</DetailItem> </DetailItem>
<DetailItem <DetailItem
label={intl.get('vendor_credit.drawer.label_vendor_credit_no')} label={intl.get('vendor_credit.drawer.label_vendor_credit_no')}
@@ -78,7 +78,7 @@ export default function VendorCreditDetailHeader() {
/> />
<DetailItem <DetailItem
label={<T id={'vendor_credit.drawer.label_created_at'} />} label={<T id={'vendor_credit.drawer.label_created_at'} />}
children={<FormatDate value={vendorCredit.created_at} />} children={vendorCredit.formatted_created_at}
/> />
</DetailsMenu> </DetailsMenu>
</Col> </Col>

View File

@@ -11,7 +11,7 @@ import {
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { FormattedMessage as T } from '@/components'; import { Group, FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
@@ -78,7 +78,10 @@ export default function ExpenseFloatingFooter() {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Publish ----------- */} {/* ----------- Save And Publish ----------- */}
<If condition={isNewMode}> <If condition={isNewMode}>
<ButtonGroup> <ButtonGroup>
@@ -190,6 +193,6 @@ export default function ExpenseFloatingFooter() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -17,13 +17,7 @@ import clsx from 'classnames';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { ExpenseAction, AbilitySubject } from '@/constants/abilityOption'; import { ExpenseAction, AbilitySubject } from '@/constants/abilityOption';
import { import { FormattedMessage as T, Icon, If, Can } from '@/components';
FormatDateCell,
FormattedMessage as T,
Icon,
If,
Can,
} from '@/components';
import { safeCallback } from '@/utils'; import { safeCallback } from '@/utils';
/** /**
@@ -137,8 +131,7 @@ export function useExpensesTableColumns() {
{ {
id: 'payment_date', id: 'payment_date',
Header: intl.get('payment_date'), Header: intl.get('payment_date'),
accessor: 'payment_date', accessor: 'formatted_date',
Cell: FormatDateCell,
width: 140, width: 140,
className: 'payment_date', className: 'payment_date',
clickable: true, clickable: true,

View File

@@ -96,12 +96,19 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
} }
} }
} }
&:not(:first-child).is-expanded .td {
border-top: 1px solid #ddd;
}
} }
&--OPENING_BALANCE, &--OPENING_BALANCE,
&--CLOSING_BALANCE { &--CLOSING_BALANCE {
.td {
color: #000;
}
.date {
font-weight: 500;
.cell-inner {
position: absolute;
}
}
.amount { .amount {
font-weight: 500; font-weight: 500;
} }
@@ -110,6 +117,9 @@ const GeneralLedgerDataTable = styled(ReportDataTable)`
.name { .name {
font-weight: 500; font-weight: 500;
} }
.td {
border-top: 1px solid #ddd;
}
} }
} }
} }

View File

@@ -5,9 +5,8 @@ import classNames from 'classnames';
import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core'; import { Button, Intent, FormGroup, Checkbox } from '@blueprintjs/core';
import { FastField, useFormikContext } from 'formik'; import { FastField, useFormikContext } from 'formik';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useItemFormContext } from './ItemFormProvider'; import { useItemFormContext } from './ItemFormProvider';
import { FormattedMessage as T } from '@/components'; import { Group, FormattedMessage as T } from '@/components';
import { saveInvoke } from '@/utils'; import { saveInvoke } from '@/utils';
/** /**
@@ -37,7 +36,10 @@ export default function ItemFormFloatingActions({ onCancel }) {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
<SaveButton <SaveButton
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
disabled={isSubmitting} disabled={isSubmitting}
@@ -78,7 +80,7 @@ export default function ItemFormFloatingActions({ onCancel }) {
</FormGroup> </FormGroup>
)} )}
</FastField> </FastField>
</div> </Group>
); );
} }

View File

@@ -3,7 +3,6 @@ import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import ItemForm from './ItemForm'; import ItemForm from './ItemForm';
/** /**
* Item form page. * Item form page.
*/ */

View File

@@ -1,16 +1,13 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import moment from 'moment';
export const useGLEntriesTableColumns = () => { export const useGLEntriesTableColumns = () => {
return React.useMemo( return React.useMemo(
() => [ () => [
{ {
Header: intl.get('date'), Header: intl.get('date'),
accessor: ({ formatted_date }) => accessor: 'date.formatted_date',
moment(formatted_date).format('YYYY MMM DD'),
width: 140, width: 140,
className: 'date', className: 'date',
textOverview: true, textOverview: true,
@@ -28,14 +25,6 @@ export const useGLEntriesTableColumns = () => {
width: 140, width: 140,
textOverview: true, textOverview: true,
}, },
{
Header: intl.get('credit'),
accessor: ({ credit }) => credit.formatted_amount,
width: 100,
className: 'credit',
align: 'right',
textOverview: true,
},
{ {
Header: intl.get('debit'), Header: intl.get('debit'),
accessor: ({ debit }) => debit.formatted_amount, accessor: ({ debit }) => debit.formatted_amount,
@@ -44,6 +33,14 @@ export const useGLEntriesTableColumns = () => {
textOverview: true, textOverview: true,
align: 'right', align: 'right',
}, },
{
Header: intl.get('credit'),
accessor: ({ credit }) => credit.formatted_amount,
width: 100,
className: 'credit',
align: 'right',
textOverview: true,
},
], ],
[], [],
); );

View File

@@ -10,7 +10,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { FormattedMessage as T } from '@/components'; import { Group, FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -76,7 +76,10 @@ export default function BillFloatingActions() {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Open ----------- */} {/* ----------- Save And Open ----------- */}
<If condition={!bill || !bill?.is_open}> <If condition={!bill || !bill?.is_open}>
<ButtonGroup> <ButtonGroup>
@@ -189,6 +192,6 @@ export default function BillFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -161,8 +161,7 @@ export function useBillsTableColumns() {
{ {
id: 'bill_date', id: 'bill_date',
Header: intl.get('bill_date'), Header: intl.get('bill_date'),
accessor: 'bill_date', accessor: 'formatted_bill_date',
Cell: FormatDateCell,
width: 110, width: 110,
className: 'bill_date', className: 'bill_date',
clickable: true, clickable: true,

View File

@@ -13,7 +13,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { If, Icon, FormattedMessage as T } from '@/components'; import { If, Icon, FormattedMessage as T, Group } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider'; import { useVendorCreditNoteFormContext } from './VendorCreditNoteFormProvider';
@@ -69,11 +69,15 @@ export default function VendorCreditNoteFloatingActions() {
history.goBack(); history.goBack();
}; };
// Handle the clear button click.
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
resetForm(); resetForm();
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Open ----------- */} {/* ----------- Save And Open ----------- */}
<If condition={!vendorCredit || !vendorCredit?.is_open}> <If condition={!vendorCredit || !vendorCredit?.is_open}>
<ButtonGroup> <ButtonGroup>
@@ -185,6 +189,6 @@ export default function VendorCreditNoteFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -5,14 +5,7 @@ import clsx from 'classnames';
import { Intent, Tag, Menu, MenuItem, MenuDivider } from '@blueprintjs/core'; import { Intent, Tag, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { import { FormattedMessage as T, Choose, If, Icon, Can } from '@/components';
FormatDateCell,
FormattedMessage as T,
Choose,
If,
Icon,
Can,
} from '@/components';
import { safeCallback } from '@/utils'; import { safeCallback } from '@/utils';
import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption'; import { VendorCreditAction, AbilitySubject } from '@/constants/abilityOption';
@@ -119,7 +112,6 @@ export function useVendorsCreditNoteTableColumns() {
id: 'credit_date', id: 'credit_date',
Header: intl.get('date'), Header: intl.get('date'),
accessor: 'formatted_vendor_credit_date', accessor: 'formatted_vendor_credit_date',
Cell: FormatDateCell,
width: 110, width: 110,
className: 'credit_date', className: 'credit_date',
clickable: true, clickable: true,

View File

@@ -11,14 +11,12 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from '@/components'; import { Group, Icon, FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider'; import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
/** /**
* Payment made floating actions bar. * Payment made floating actions bar.
*/ */
@@ -56,11 +54,14 @@ export default function PaymentMadeFloatingActions() {
// Handle submit & continue editing button click. // Handle submit & continue editing button click.
const handleSubmitContinueEditingBtnClick = (event) => { const handleSubmitContinueEditingBtnClick = (event) => {
setSubmitPayload({ redirect: false, publish: true }); setSubmitPayload({ redirect: false, publish: true });
submitForm() submitForm();
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<ButtonGroup> <ButtonGroup>
<Button <Button
@@ -109,6 +110,6 @@ export default function PaymentMadeFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -11,7 +11,7 @@ import {
Position, Position,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Icon, Money, FormatDateCell, Can } from '@/components'; import { Icon, Money, Can } from '@/components';
import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption'; import { PaymentMadeAction, AbilitySubject } from '@/constants/abilityOption';
import { safeCallback } from '@/utils'; import { safeCallback } from '@/utils';
@@ -29,7 +29,7 @@ export function ActionsMenu({
}) { }) {
return ( return (
<Menu> <Menu>
<MenuItem <MenuItem
icon={<Icon icon="reader-18" />} icon={<Icon icon="reader-18" />}
text={intl.get('view_details')} text={intl.get('view_details')}
onClick={safeCallback(onViewDetails, original)} onClick={safeCallback(onViewDetails, original)}
@@ -79,8 +79,7 @@ export function usePaymentMadesTableColumns() {
{ {
id: 'payment_date', id: 'payment_date',
Header: intl.get('payment_date'), Header: intl.get('payment_date'),
Cell: FormatDateCell, accessor: 'formatted_payment_date',
accessor: 'payment_date',
width: 140, width: 140,
className: 'payment_date', className: 'payment_date',
clickable: true, clickable: true,

View File

@@ -12,7 +12,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { If, Icon, FormattedMessage as T } from '@/components'; import { If, Icon, FormattedMessage as T, Group } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCreditNoteFormContext } from './CreditNoteFormProvider'; import { useCreditNoteFormContext } from './CreditNoteFormProvider';
@@ -69,12 +69,16 @@ export default function CreditNoteFloatingActions() {
history.goBack(); history.goBack();
}; };
// Handle clear button click.
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
resetForm(); resetForm();
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Open ----------- */} {/* ----------- Save And Open ----------- */}
<If condition={!creditNote || !creditNote?.is_open}> <If condition={!creditNote || !creditNote?.is_open}>
<ButtonGroup> <ButtonGroup>
@@ -186,6 +190,6 @@ export default function CreditNoteFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -5,7 +5,6 @@ import clsx from 'classnames';
import { Intent, Tag, Menu, MenuItem, MenuDivider } from '@blueprintjs/core'; import { Intent, Tag, Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { import {
FormatDateCell,
FormattedMessage as T, FormattedMessage as T,
Choose, Choose,
If, If,
@@ -112,7 +111,6 @@ export function useCreditNoteTableColumns() {
id: 'credit_date', id: 'credit_date',
Header: intl.get('credit_note.column.credit_date'), Header: intl.get('credit_note.column.credit_date'),
accessor: 'formatted_credit_note_date', accessor: 'formatted_credit_note_date',
Cell: FormatDateCell,
width: 110, width: 110,
className: 'credit_date', className: 'credit_date',
clickable: true, clickable: true,

View File

@@ -11,7 +11,7 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { If, Icon, FormattedMessage as T } from '@/components'; import { If, Icon, FormattedMessage as T, Group } from '@/components';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
@@ -63,16 +63,21 @@ export default function EstimateFloatingActions() {
submitForm(); submitForm();
}; };
// Handle the cancel button click.
const handleCancelBtnClick = (event) => { const handleCancelBtnClick = (event) => {
history.goBack(); history.goBack();
}; };
// Handle the clear button click.
const handleClearBtnClick = (event) => { const handleClearBtnClick = (event) => {
resetForm(); resetForm();
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Deliver ----------- */} {/* ----------- Save And Deliver ----------- */}
<If condition={!estimate || !estimate?.is_delivered}> <If condition={!estimate || !estimate?.is_delivered}>
<ButtonGroup> <ButtonGroup>
@@ -188,6 +193,6 @@ export default function EstimateFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -164,8 +164,7 @@ export function useEstiamtesTableColumns() {
{ {
id: 'estimate_date', id: 'estimate_date',
Header: intl.get('estimate_date'), Header: intl.get('estimate_date'),
accessor: 'estimate_date', accessor: 'formatted_estimate_date',
Cell: FormatDateCell,
width: 140, width: 140,
className: 'estimate_date', className: 'estimate_date',
clickable: true, clickable: true,

View File

@@ -13,7 +13,7 @@ import {
import classNames from 'classnames'; import classNames from 'classnames';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { If, Icon, FormattedMessage as T } from '@/components'; import { If, Icon, FormattedMessage as T, Group } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useInvoiceFormContext } from './InvoiceFormProvider'; import { useInvoiceFormContext } from './InvoiceFormProvider';
@@ -76,7 +76,10 @@ export default function InvoiceFloatingActions() {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save And Deliver ----------- */} {/* ----------- Save And Deliver ----------- */}
<If condition={!invoice || !invoice?.is_delivered}> <If condition={!invoice || !invoice?.is_delivered}>
<ButtonGroup> <ButtonGroup>
@@ -189,6 +192,6 @@ export default function InvoiceFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

View File

@@ -70,6 +70,17 @@ export const defaultInvoice = {
attachments: [], attachments: [],
}; };
// Invoice entry request schema.
export const defaultReqInvoiceEntry = {
index: 0,
item_id: '',
rate: '',
discount: '',
quantity: '',
description: '',
tax_rate_id: '',
};
/** /**
* Transform invoice to initial values in edit mode. * Transform invoice to initial values in edit mode.
*/ */
@@ -175,13 +186,27 @@ export const ITEMS_FILTER_ROLES_QUERY = JSON.stringify([
}, },
]); ]);
/**
* Transformes bill entries to submit request.
*/
const transformEntriesToRequest = (entries) => {
return R.compose(
R.map(R.compose(R.curry(transformToForm)(R.__, defaultReqInvoiceEntry))),
filterNonZeroEntries,
)(entries);
};
/**
* Filters the givne non-zero entries.
*/
const filterNonZeroEntries = (entries) => {
return entries.filter((item) => item.item_id && item.quantity);
};
/** /**
* Transformes the form values to request body values. * Transformes the form values to request body values.
*/ */
export function transformValueToRequest(values) { export function transformValueToRequest(values) {
const entries = values.entries.filter(
(item) => item.item_id && item.quantity,
);
return { return {
...omit(values, [ ...omit(values, [
'invoice_no', 'invoice_no',
@@ -194,9 +219,7 @@ export function transformValueToRequest(values) {
invoice_no: values.invoice_no, invoice_no: values.invoice_no,
}), }),
is_inclusive_tax: values.inclusive_exclusive_tax === TaxType.Inclusive, is_inclusive_tax: values.inclusive_exclusive_tax === TaxType.Inclusive,
entries: entries.map((entry) => ({ entries: transformEntriesToRequest(values.entries),
...omit(entry, ['amount', 'tax_amount', 'tax_rate']),
})),
delivered: false, delivered: false,
attachments: transformAttachmentsToRequest(values), attachments: transformAttachmentsToRequest(values),
}; };

View File

@@ -128,7 +128,7 @@ export function ActionsMenu({
onQuick, onQuick,
onViewDetails, onViewDetails,
onPrint, onPrint,
onSendMail onSendMail,
}, },
row: { original }, row: { original },
}) { }) {
@@ -202,8 +202,7 @@ export function useInvoicesTableColumns() {
{ {
id: 'invoice_date', id: 'invoice_date',
Header: intl.get('invoice_date'), Header: intl.get('invoice_date'),
accessor: 'invoice_date', accessor: 'invoice_date_formatted',
Cell: FormatDateCell,
width: 110, width: 110,
className: 'invoice_date', className: 'invoice_date',
clickable: true, clickable: true,

View File

@@ -11,12 +11,11 @@ import {
Menu, Menu,
MenuItem, MenuItem,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { Icon, FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Group, Icon, FormattedMessage as T } from '@/components';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { CLASSES } from '@/constants/classes';
import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider'; import { usePaymentReceiveFormContext } from './PaymentReceiveFormProvider';
import { CLASSES } from '@/constants/classes';
/** /**
* Payment receive floating actions bar. * Payment receive floating actions bar.
@@ -55,7 +54,10 @@ export default function PaymentReceiveFormFloatingActions() {
}; };
return ( return (
<div className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}> <Group
spacing={10}
className={classNames(CLASSES.PAGE_FORM_FLOATING_ACTIONS)}
>
{/* ----------- Save and New ----------- */} {/* ----------- Save and New ----------- */}
<ButtonGroup> <ButtonGroup>
<Button <Button
@@ -107,6 +109,6 @@ export default function PaymentReceiveFormFloatingActions() {
onClick={handleCancelBtnClick} onClick={handleCancelBtnClick}
text={<T id={'cancel'} />} text={<T id={'cancel'} />}
/> />
</div> </Group>
); );
} }

Some files were not shown because too many files have changed in this diff Show More