Compare commits

..

49 Commits

Author SHA1 Message Date
a.bouhuolia
e1ea5c402c feat: add env variable to customize the proxy public ports 2023-05-31 20:29:37 +02:00
a.bouhuolia
34b2c2c8b4 fix: copy package-lock.json inside the container 2023-05-31 13:07:30 +02:00
a.bouhuolia
5d96fe6aa0 chore(webapp): remove Sentry from webapp 2023-05-31 09:59:35 +02:00
Ameir Abdeldayem
d2b5084b42 chore: fix typo in README file (#124) 2023-05-30 10:46:51 +02:00
a.bouhuolia
81fb0734d5 update CHANGELOG 2023-05-28 15:04:31 +02:00
a.bouhuolia
3639ce44e5 chore: bump CHANGELOG v0.9.1 2023-05-28 15:02:14 +02:00
Ahmed Bouhuolia
a7c00d60d5 Merge pull request #121 from bigcapitalhq/BIG-429-clean-up-the-auto-increment-of-transactions
fix: the auto-increment of transactions.
2023-05-28 14:50:43 +02:00
a.bouhuolia
932750b62d fix(webapp): fix credit note and receipt auto-increment 2023-05-28 14:45:34 +02:00
a.bouhuolia
c90ffed67f fix(webapp): payment receive auto-increment 2023-05-26 00:02:47 +02:00
a.bouhuolia
e92c4486aa fix(webapp): auto-increment estimate transactions 2023-05-25 22:04:21 +02:00
a.bouhuolia
aaceea5338 fix(webapp): warehouse and branch reset on invoice form 2023-05-24 23:52:05 +02:00
a.bouhuolia
4d54d180bc fix(webapp): invoice transactions increment 2023-05-24 23:28:09 +02:00
Ahmed Bouhuolia
8fdd98e34d Merge pull request #122 from bigcapitalhq/BIG-434-delete-invoice-transaction-issue
fix(server): delete invoice transaction
2023-05-23 15:12:43 +02:00
a.bouhuolia
d53c5ee5e6 fix(server): delete invoice transaction 2023-05-23 15:11:56 +02:00
a.bouhuolia
4082e4e2b8 fix: auto-increment transaction field 2023-05-23 14:39:57 +02:00
a.bouhuolia
0c689459cb fix: auto-increment cashflow transactions 2023-05-23 13:56:35 +02:00
a.bouhuolia
40ef02f215 fix: auto-increment settings 2023-05-22 21:57:43 +02:00
a.bouhuolia
d369f0bb17 fix: the auto-increment of transactions. 2023-05-19 00:29:35 +02:00
Ahmed Bouhuolia
425d0293cc Merge pull request #120 from bigcapitalhq/BIG-433-fix-base-currency-should-be-enabled-with-account-model
BIG-433-fix-base-currency-should-be-enabled-with-account-model
2023-05-12 15:58:48 +02:00
a.bouhuolia
b621650975 fix(server): base currency should be enabled with account model. 2023-05-12 15:58:01 +02:00
a.bouhuolia
40948160fe fix(webapp): localization 2023-05-12 12:46:59 +02:00
Ahmed Bouhuolia
aa6b9dd295 Merge pull request #118 from bigcapitalhq/BIG-428-clean-up-the-preferences-pages
fix(webapp): general, accoutant and items preferences
2023-05-12 12:36:19 +02:00
a.bouhuolia
05c2232b97 chore(webapp): refactor the setup organization form to use Formik binded component 2023-05-12 12:31:55 +02:00
Ahmed Bouhuolia
8f6325d529 Merge pull request #119 from bigcapitalhq/fix-delete-journals-manual-journal
fix(server): deleting ledger entries of manual journal
2023-05-12 00:14:36 +02:00
a.bouhuolia
0aa681043d fix(server): deleting ledger entries of manual journal 2023-05-11 22:46:34 +02:00
a.bouhuolia
40bddfdfeb fix(webapp): accrual typo 2023-05-11 21:07:49 +02:00
a.bouhuolia
d6e2f01d70 fix(server): accrual typo 2023-05-11 21:07:01 +02:00
a.bouhuolia
2344d3d34d fix(webapp): general, accoutant and items preferences 2023-05-11 01:47:09 +02:00
a.bouhuolia
883c5dcb41 Merge branch 'signup-restrictions' into develop 2023-05-08 00:36:50 +02:00
a.bouhuolia
be10b8934d fix(webapp): change the error code handler 2023-05-08 00:35:44 +02:00
a.bouhuolia
ce38c71fa7 fix(server): should allowed email addresses and domain be irrespective. 2023-05-08 00:35:28 +02:00
Ahmed Bouhuolia
1162fbc7c3 Merge pull request #117 from bigcapitalhq/signup-restrictions
Sign-up restrictions for self-hosted
2023-05-08 00:18:56 +02:00
a.bouhuolia
18b9e25f2b chore: update .env.example 2023-05-07 23:59:41 +02:00
a.bouhuolia
dd26bdc482 feat(webapp): sign-up restrictions 2023-05-07 23:54:42 +02:00
a.bouhuolia
ad3c9ebfe9 feat(server): sign-up restrictions for self-hosted 2023-05-07 17:22:18 +02:00
a.bouhuolia
36611652da fix(webapp): resource meta of vendors list 2023-05-05 15:41:32 +02:00
a.bouhuolia
06c7ee71b4 fix(webapp): display transactions count in cashflow account 2023-05-05 13:54:45 +02:00
Ahmed Bouhuolia
54d3188666 Merge pull request #116 from bigcapitalhq/BIG-427-fix-sending-invite-email
fix(server): sending invite email
2023-05-05 00:30:24 +02:00
a.bouhuolia
3ceb9adda2 fix(server): sending invite email 2023-05-05 00:28:57 +02:00
Ahmed Bouhuolia
1249415054 Merge pull request #115 from bigcapitalhq/BIG-409-some-flag-icons-are-missing
fix(webapp): some flag icons are missing
2023-05-04 21:32:10 +02:00
a.bouhuolia
4d44ce4c7f fix(webapp): some flag icons are missing 2023-05-04 21:29:12 +02:00
Ahmed Bouhuolia
6c96c371c5 Merge pull request #114 from bigcapitalhq/BIG-279-select-specific-accounts-in-general-ledger-does-not-working
`BIG-279` Select specific accounts in general ledger does not working.
2023-05-04 14:29:35 +02:00
a.bouhuolia
6c61a69f10 feat(webapp): handle create item on Accounts select components 2023-05-04 14:24:45 +02:00
a.bouhuolia
981b65349d feat(webapp): allow to create a new account item in accounts list component. 2023-05-03 22:41:54 +02:00
a.bouhuolia
a7d29a31c8 refactor(webapp): all services with new AccountSelect and AccountMultiSelect components. 2023-05-01 00:13:23 +02:00
a.bouhuolia
c1d92b74f0 chore(Select):style the Select button. 2023-04-30 21:13:33 +02:00
a.bouhuolia
6f0f47f38a refactor(webapp): Accounts Select and MultiSelect components 2023-04-30 17:33:15 +02:00
a.bouhuolia
83510cfa70 feat(server): add structure query flat or tree to accounts chart endpoint 2023-04-30 17:24:49 +02:00
a.bouhuolia
903dc0522a chore: add CONTRIBUTING.md file 2023-04-27 01:56:46 +02:00
449 changed files with 3205 additions and 7509 deletions

View File

@@ -29,6 +29,15 @@ JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
BASE_URL=https://bigcapital.ly BASE_URL=https://bigcapital.ly
CONTACT_US_MAIL=support@bigcapital.ly CONTACT_US_MAIL=support@bigcapital.ly
# App proxy
PUBLIC_PROXY_PORT=80
PUBLIC_PROXY_SSL_PORT=443
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions
SIGNUP_DISABLED=true
SIGNUP_ALLOWED_DOMAINS=
SIGNUP_ALLOWED_EMAILS=

View File

@@ -2,6 +2,24 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
## [0.9.1] - 28-05-2023
`@bigcapital/server`
- fix: deleting ledger entries of manual journal.
- fix: base currency should be enabled.
- fix: delete invoice transaction issue.
`@bigcapital/webapp`
- fix: general, accoutant and items preferences.
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
- refactor: the setup organization form to use binded Formik components.
## [0.9.0] - 06-05-2023
`@bigcapital/server`
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
## [0.8.3] - 06-04-2023 ## [0.8.3] - 06-04-2023
`@bigcaptial/monorepo` `@bigcaptial/monorepo`

View File

@@ -34,7 +34,7 @@ Contributions via pull requests are much appreciated. Once the approach is agree
## Contribute to Backend ## Contribute to Backend
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory. - Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
- Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit the command on root directory and it will install dependencies of all packages. - Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit these command on root directory and it will install dependencies of all packages.
``` ```
npm install npm install
@@ -47,7 +47,7 @@ npm run bootstrap
docker-compose up -d docker-compose up -d
``` ```
Wait some seconds, and hit `docker-compose ps` to see the result and you should see the same result below. Wait some seconds, and hit `docker-compose ps` and you should see the same result below.
``` ```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
@@ -122,7 +122,7 @@ There are many other ways to get involved with the community and to participate
- Use the product, submitting GitHub issues when a problem is found. - Use the product, submitting GitHub issues when a problem is found.
- Help code review pull requests and participate in issue threads. - Help code review pull requests and participate in issue threads.
- Submit a new feature request as an issue. - Submit a new feature request as an issue.
- Help answer questions on forums such as Stack Overflow and SigNoz Community Slack Channel. - Help answer questions on forums such as Bigcapital Community Discord Channel.
- Tell others about the project on Twitter, your blog, etc. - Tell others about the project on Twitter, your blog, etc.
**[`^top^`](#)** **[`^top^`](#)**

View File

@@ -26,6 +26,6 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs. - [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo. - [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
# Changlog # Changelog
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently. Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.

View File

@@ -15,14 +15,14 @@ services:
- ./data/logs/nginx/:/var/log/nginx - ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs - ./docker/certbot/certs/:/var/certs
ports: ports:
- "80:80" - "${PUBLIC_PROXY_PORT:-80}:80"
- "443:443" - "${PUBLIC_PROXY_SSL_PORT:-443}:443"
tty: true tty: true
depends_on: depends_on:
- server - server
- webapp - webapp
webapp: webapp:
container_name: bigcapital-webapp container_name: bigcapital-webapp
image: ghcr.io/bigcapitalhq/webapp:latest image: ghcr.io/bigcapitalhq/webapp:latest
@@ -72,6 +72,11 @@ services:
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER} - AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD} - AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
# Sign-up restrictions
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:

View File

@@ -34,7 +34,11 @@ ARG MAIL_HOST= \
BASE_URL= \ BASE_URL= \
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash \ AGENDASH_AUTH_USER=agendash \
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123 \
# Sign-up restriction
SIGNUP_DISABLED= \
SIGNUP_ALLOWED_DOMAINS= \
SIGNUP_ALLOWED_EMAILS=
ENV MAIL_HOST=$MAIL_HOST \ ENV MAIL_HOST=$MAIL_HOST \
MAIL_USERNAME=$MAIL_USERNAME \ MAIL_USERNAME=$MAIL_USERNAME \
@@ -68,7 +72,11 @@ ENV MAIL_HOST=$MAIL_HOST \
# MongoDB # MongoDB
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \ MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
# Application # Application
BASE_URL=$BASE_URL BASE_URL=$BASE_URL \
# Sign-up restriction
SIGNUP_DISABLED=$SIGNUP_DISABLED \
SIGNUP_ALLOWED_DOMAINS=$SIGNUP_ALLOWED_DOMAINS \
SIGNUP_ALLOWED_EMAILS=$SIGNUP_ALLOWED_EMAILS
# Create app directory. # Create app directory.
WORKDIR /app WORKDIR /app

View File

@@ -3,7 +3,12 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces'; import {
AbilitySubject,
AccountAction,
IAccountDTO,
IAccountsStructureType,
} from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes'; import { DATATYPES_LENGTH } from '@/data/DataTypes';
@@ -172,6 +177,11 @@ export default class AccountsController extends BaseController {
query('inactive_mode').optional().isBoolean().toBoolean(), query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(), query('search_keyword').optional({ nullable: true }).isString().trim(),
query('structure')
.optional()
.isString()
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
]; ];
} }
@@ -341,6 +351,7 @@ export default class AccountsController extends BaseController {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
inactiveMode: false, inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -49,6 +49,7 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.resetPassword.bind(this)), asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors this.handlerErrors
); );
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
return router; return router;
} }
@@ -207,6 +208,23 @@ export default class AuthenticationController extends BaseController {
} }
} }
/**
* Retrieves the authentication meta for SPA.
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns {Response|void}
*/
private async getAuthMeta(req: Request, res: Response, next: Function) {
try {
const meta = await this.authApplication.getAuthMeta();
return res.status(200).send({ meta });
} catch (error) {
next(error);
}
}
/** /**
* Handles the service errors. * Handles the service errors.
*/ */
@@ -247,6 +265,30 @@ export default class AuthenticationController extends BaseController {
errors: [{ type: 'EMAIL.EXISTS', code: 600 }], errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
}); });
} }
if (error.errorType === 'SIGNUP_RESTRICTED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED',
message:
'Sign-up is restricted no one can sign-up to the system.',
code: 700,
},
],
});
}
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
message:
'Sign-up is restricted the given email address is not allowed to sign-up.',
code: 710,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -41,7 +41,7 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
get balanceSheetValidationSchema(): ValidationChain[] { get balanceSheetValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('accounting_method').optional().isIn(['cash', 'accural']), query('accounting_method').optional().isIn(['cash', 'accrual']),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),

View File

@@ -67,6 +67,7 @@ export default class GeneralLedgerReportController extends BaseFinancialReportCo
try { try {
const { data, query, meta } = const { data, query, meta } =
await this.generalLedgetService.generalLedger(tenantId, filter); await this.generalLedgetService.generalLedger(tenantId, filter);
return res.status(200).send({ return res.status(200).send({
meta: this.transfromToResponse(meta), meta: this.transfromToResponse(meta),
data: this.transfromToResponse(data), data: this.transfromToResponse(data),

View File

@@ -58,7 +58,7 @@ export default class OrganizationController extends BaseController {
private get organizationValidationSchema(): ValidationChain[] { private get organizationValidationSchema(): ValidationChain[] {
return [ return [
check('name').exists().trim(), check('name').exists().trim(),
check('industry').optional().isString(), check('industry').optional({ nullable: true }).isString().trim().escape(),
check('location').exists().isString().isISO31661Alpha2(), check('location').exists().isString().isISO31661Alpha2(),
check('base_currency').exists().isISO4217(), check('base_currency').exists().isISO4217(),
check('timezone').exists().isIn(moment.tz.names()), check('timezone').exists().isIn(moment.tz.names()),

View File

@@ -4,6 +4,7 @@ import moment from 'moment';
global.__root_dir = path.join(__dirname, '..'); global.__root_dir = path.join(__dirname, '..');
global.__resources_dir = path.join(global.__root_dir, 'resources'); global.__resources_dir = path.join(global.__root_dir, 'resources');
global.__locales_dir = path.join(global.__resources_dir, 'locales'); global.__locales_dir = path.join(global.__resources_dir, 'locales');
global.__views_dir = path.join(global.__root_dir, 'views');
moment.prototype.toMySqlDateTime = function () { moment.prototype.toMySqlDateTime = function () {
return this.format('YYYY-MM-DD HH:mm:ss'); return this.format('YYYY-MM-DD HH:mm:ss');

View File

@@ -1,5 +1,6 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { castCommaListEnvVarToArray, parseBoolean } from '@/utils';
dotenv.config(); dotenv.config();
@@ -146,6 +147,19 @@ module.exports = {
}, },
}, },
/**
* Sign-up restrictions
*/
signupRestrictions: {
disabled: parseBoolean<boolean>(process.env.SIGNUP_DISABLED, false),
allowedDomains: castCommaListEnvVarToArray(
process.env.SIGNUP_ALLOWED_DOMAINS
),
allowedEmails: castCommaListEnvVarToArray(
process.env.SIGNUP_ALLOWED_EMAILS
),
},
/** /**
* Puppeteer remote browserless connection. * Puppeteer remote browserless connection.
*/ */

View File

@@ -3,17 +3,17 @@ import AccountsData from '../data/accounts';
export default class SeedAccounts extends TenantSeeder { export default class SeedAccounts extends TenantSeeder {
/** /**
* Seeds initial accounts to the organization. * Seeds initial accounts to the organization.
*/ */
up(knex) { up(knex) {
const data = AccountsData.map((account) => { const data = AccountsData.map((account) => ({
return { ...account,
...account, name: this.i18n.__(account.name),
name: this.i18n.__(account.name), description: this.i18n.__(account.description),
description: this.i18n.__(account.description), currencyCode: this.tenant.metadata.baseCurrency,
currencyCode: this.tenant.metadata.baseCurrency, seededAt: new Date(),
}; })
}); );
return knex('accounts').then(async () => { return knex('accounts').then(async () => {
// Inserts seed entries. // Inserts seed entries.
return knex('accounts').insert(data); return knex('accounts').insert(data);

View File

@@ -8,7 +8,7 @@ export default class SeedSettings extends TenantSeeder {
up() { up() {
const settings = [ const settings = [
// Orgnization settings. // Orgnization settings.
{ group: 'organization', key: 'accounting_basis', value: 'accural' }, { group: 'organization', key: 'accounting_basis', value: 'accrual' },
// Accounts settings. // Accounts settings.
{ group: 'accounts', key: 'account_code_unique', value: true }, { group: 'accounts', key: 'account_code_unique', value: true },

View File

@@ -79,9 +79,15 @@ export interface IAccountTransaction {
} }
export interface IAccountResponse extends IAccount {} export interface IAccountResponse extends IAccount {}
export enum IAccountsStructureType {
Tree = 'tree',
Flat = 'flat',
}
export interface IAccountsFilter extends IDynamicListFilterDTO { export interface IAccountsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
onlyInactive: boolean; onlyInactive: boolean;
structure?: IAccountsStructureType;
} }
export interface IAccountType { export interface IAccountType {

View File

@@ -74,4 +74,8 @@ export interface IAuthSendingResetPassword {
export interface IAuthSendedResetPassword { export interface IAuthSendedResetPassword {
user: ISystemUser, user: ISystemUser,
token: string; token: string;
}
export interface IAuthGetMetaPOJO {
signupDisabled: boolean;
} }

View File

@@ -44,7 +44,7 @@ export interface IBalanceSheetQuery extends IFinancialSheetBranchesQuery {
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
noneTransactions: boolean; noneTransactions: boolean;
noneZero: boolean; noneZero: boolean;
basis: 'cash' | 'accural'; basis: 'cash' | 'accrual';
accountIds: number[]; accountIds: number[];
percentageOfColumn: boolean; percentageOfColumn: boolean;

View File

@@ -4,7 +4,7 @@ export interface ITrialBalanceSheetQuery {
fromDate: Date | string; fromDate: Date | string;
toDate: Date | string; toDate: Date | string;
numberFormat: INumberFormatQuery; numberFormat: INumberFormatQuery;
basis: 'cash' | 'accural'; basis: 'cash' | 'accrual';
noneZero: boolean; noneZero: boolean;
noneTransactions: boolean; noneTransactions: boolean;
onlyActive: boolean; onlyActive: boolean;

View File

@@ -1,6 +1,7 @@
import { AnyObject } from '@casl/ability/dist/types/types'; import { AnyObject } from '@casl/ability/dist/types/types';
import { ITenant } from '@/interfaces'; import { ITenant } from '@/interfaces';
import { Model } from 'objection'; import { Model } from 'objection';
import { Tenant } from '@/system/models';
export interface ISystemUser extends Model { export interface ISystemUser extends Model {
id: number; id: number;
@@ -54,20 +55,52 @@ export interface IUserInvite {
export interface IInviteUserService { export interface IInviteUserService {
acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void>; acceptInvite(token: string, inviteUserInput: IInviteUserInput): Promise<void>;
/**
* Re-send user invite.
* @param {number} tenantId -
* @param {string} email -
* @return {Promise<{ invite: IUserInvite }>}
*/
resendInvite( resendInvite(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
invite: IUserInvite; user: ITenantUser;
}>; }>;
/**
* Sends invite mail to the given email from the given tenant and user.
* @param {number} tenantId -
* @param {string} email -
* @param {IUser} authorizedUser -
* @return {Promise<IUserInvite>}
*/
sendInvite( sendInvite(
tenantId: number, tenantId: number,
email: string, sendInviteDTO: IUserSendInviteDTO,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{
invite: IUserInvite; invitedUser: ITenantUser;
}>; }>;
}
export interface IAcceptInviteUserService {
/**
* Accept the received invite.
* @param {string} token
* @param {IInviteUserInput} inviteUserInput
* @throws {ServiceErrors}
* @returns {Promise<void>}
*/
acceptInvite(token: string, inviteUserDTO: IInviteUserInput): Promise<void>;
/**
* Validate the given invite token.
* @param {string} token - the given token string.
* @throws {ServiceError}
*/
checkInvite( checkInvite(
token: string token: string
): Promise<{ inviteToken: IUserInvite; orgName: object }>; ): Promise<{ inviteToken: IUserInvite; orgName: object }>;
@@ -121,7 +154,7 @@ export interface IUserInvitedEventPayload {
tenantId: number; tenantId: number;
user: ITenantUser; user: ITenantUser;
} }
export interface IUserInviteTenantSyncedEventPayload{ export interface IUserInviteTenantSyncedEventPayload {
invite: IUserInvite; invite: IUserInvite;
authorizedUser: ISystemUser; authorizedUser: ISystemUser;
tenantId: number; tenantId: number;
@@ -143,10 +176,10 @@ export interface IAcceptInviteEventPayload {
export interface ICheckInviteEventPayload { export interface ICheckInviteEventPayload {
inviteToken: IUserInvite; inviteToken: IUserInvite;
tenant: ITenant tenant: Tenant;
} }
export interface IUserSendInviteDTO { export interface IUserSendInviteDTO {
email: string; email: string;
roleId: number; roleId: number;
} }

View File

@@ -1,5 +1,6 @@
import { Container, Inject } from 'typedi'; import { Container, Inject } from 'typedi';
import InviteUserService from '@/services/InviteUsers/AcceptInviteUser'; import InviteUserService from '@/services/InviteUsers/AcceptInviteUser';
import SendInviteUsersMailMessage from '@/services/InviteUsers/SendInviteUsersMailMessage';
export default class UserInviteMailJob { export default class UserInviteMailJob {
/** /**
@@ -21,24 +22,17 @@ export default class UserInviteMailJob {
*/ */
public async handler(job, done: Function): Promise<void> { public async handler(job, done: Function): Promise<void> {
const { invite, authorizedUser, tenantId } = job.attrs.data; const { invite, authorizedUser, tenantId } = job.attrs.data;
const sendInviteMailMessage = Container.get(SendInviteUsersMailMessage);
const Logger = Container.get('logger');
const inviteUsersService = Container.get(InviteUserService);
Logger.info(`Send invite user mail - started: ${job.attrs.data}`);
try { try {
await inviteUsersService.mailMessages.sendInviteMail( await sendInviteMailMessage.sendInviteMail(
tenantId, tenantId,
authorizedUser, authorizedUser,
invite invite
); );
Logger.info(`Send invite user mail - finished: ${job.attrs.data}`);
done(); done();
} catch (error) { } catch (error) {
Logger.info( console.log(error);
`Send invite user mail - error: ${job.attrs.data}, error: ${error}`
);
done(error); done(error);
} }
} }

View File

@@ -109,7 +109,7 @@ export default class Mail {
* Retrieve view content from the view directory. * Retrieve view content from the view directory.
*/ */
private getViewContent(): string { private getViewContent(): string {
const filePath = path.join(global.__root_dir, `../views/${this.view}`); const filePath = path.join(global.__views_dir, `/${this.view}`);
return fs.readFileSync(filePath, 'utf8'); return fs.readFileSync(filePath, 'utf8');
} }
} }

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber } from 'utils'; import { formatNumber } from 'utils';
import { isArrayLikeObject } from 'lodash/fp';
export class Transformer { export class Transformer {
public context: any; public context: any;
@@ -39,12 +40,33 @@ export class Transformer {
return object; return object;
}; };
/**
*
* @param object
* @returns
*/
protected preCollectionTransform = (object: any) => {
return object;
};
/**
*
* @param object
* @returns
*/
protected postCollectionTransform = (object: any) => {
return object;
};
/** /**
* *
*/ */
public work = (object: any) => { public work = (object: any) => {
if (Array.isArray(object)) { if (Array.isArray(object)) {
return object.map(this.getTransformation); const preTransformed = this.preCollectionTransform(object);
const transformed = preTransformed.map(this.getTransformation);
return this.postCollectionTransform(transformed);
} else if (isObject(object)) { } else if (isObject(object)) {
return this.getTransformation(object); return this.getTransformation(object);
} }

View File

@@ -22,7 +22,7 @@ import SaleInvoiceAutoIncrementSubscriber from '@/subscribers/SaleInvoices/AutoI
import SaleInvoiceConvertFromEstimateSubscriber from '@/subscribers/SaleInvoices/ConvertFromEstimate'; import SaleInvoiceConvertFromEstimateSubscriber from '@/subscribers/SaleInvoices/ConvertFromEstimate';
import PaymentReceiveAutoSerialSubscriber from '@/subscribers/PaymentReceive/AutoSerialIncrement'; import PaymentReceiveAutoSerialSubscriber from '@/subscribers/PaymentReceive/AutoSerialIncrement';
import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite'; import SyncSystemSendInvite from '@/services/InviteUsers/SyncSystemSendInvite';
import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotification'; import InviteSendMainNotification from '@/services/InviteUsers/InviteSendMailNotificationSubscribe';
import SyncTenantAcceptInvite from '@/services/InviteUsers/SyncTenantAcceptInvite'; import SyncTenantAcceptInvite from '@/services/InviteUsers/SyncTenantAcceptInvite';
import SyncTenantUserMutate from '@/services/Users/SyncTenantUserSaved'; import SyncTenantUserMutate from '@/services/Users/SyncTenantUserSaved';
import { SyncTenantUserDelete } from '@/services/Users/SyncTenantUserDeleted'; import { SyncTenantUserDelete } from '@/services/Users/SyncTenantUserDeleted';

View File

@@ -10,7 +10,7 @@ export class LedgerRevert {
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject() @Inject()
ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
/** /**
* Reverts the jouranl entries. * Reverts the jouranl entries.

View File

@@ -1,6 +1,11 @@
import { IAccount } from '@/interfaces'; import { IAccount, IAccountsStructureType } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer'; import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils'; import {
assocDepthLevelToObjectTree,
flatToNestedArray,
formatNumber,
nestedArrayToFlatten,
} from 'utils';
export class AccountTransformer extends Transformer { export class AccountTransformer extends Transformer {
/** /**
@@ -8,7 +13,23 @@ export class AccountTransformer extends Transformer {
* @returns {Array} * @returns {Array}
*/ */
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return ['formattedAmount']; return ['formattedAmount', 'flattenName'];
};
/**
* Retrieves the flatten name with all dependants accounts names.
* @param {IAccount} account -
* @returns {string}
*/
public flattenName = (account: IAccount): string => {
const parentDependantsIds = this.options.accountsGraph.dependantsOf(
account.id
);
const prefixAccounts = parentDependantsIds.map((dependId) => {
const node = this.options.accountsGraph.getNodeData(dependId);
return `${node.name}: `;
});
return `${prefixAccounts}${account.name}`;
}; };
/** /**
@@ -17,8 +38,28 @@ export class AccountTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedAmount = (account: IAccount): string => { protected formattedAmount = (account: IAccount): string => {
return formatNumber(account.amount, { return formatNumber(account.amount, { currencyCode: account.currencyCode });
currencyCode: account.currencyCode, };
/**
* Transformes the accounts collection to flat or nested array.
* @param {IAccount[]}
* @returns {IAccount[]}
*/
protected postCollectionTransform = (accounts: IAccount[]) => {
// Transfom the flatten to accounts tree.
const transformed = flatToNestedArray(accounts, {
id: 'id',
parentId: 'parentAccountId',
}); });
// Associate `accountLevel` attr to indicate object depth.
const transformed2 = assocDepthLevelToObjectTree(
transformed,
1,
'accountLevel'
);
return this.options.structure === IAccountsStructureType.Flat
? nestedArrayToFlatten(transformed2)
: transformed2;
}; };
} }

View File

@@ -22,15 +22,19 @@ export class GetAccount {
*/ */
public getAccount = async (tenantId: number, accountId: number) => { public getAccount = async (tenantId: number, accountId: number) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Find the given account or throw not found error. // Find the given account or throw not found error.
const account = await Account.query().findById(accountId).throwIfNotFound(); const account = await Account.query().findById(accountId).throwIfNotFound();
const accountsGraph = await accountRepository.getDependencyGraph();
// Transformes the account model to POJO. // Transformes the account model to POJO.
const transformed = await this.transformer.transform( const transformed = await this.transformer.transform(
tenantId, tenantId,
account, account,
new AccountTransformer() new AccountTransformer(),
{ accountsGraph }
); );
return this.i18nService.i18nApply( return this.i18nService.i18nApply(
[['accountTypeLabel'], ['accountNormalFormatted']], [['accountTypeLabel'], ['accountNormalFormatted']],

View File

@@ -1,6 +1,11 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import { IAccountsFilter, IAccountResponse, IFilterMeta } from '@/interfaces'; import {
IAccountsFilter,
IAccountResponse,
IFilterMeta,
IAccountsStructureType,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { AccountTransformer } from './AccountTransform'; import { AccountTransformer } from './AccountTransform';
@@ -38,6 +43,7 @@ export class GetAccounts {
filterDTO: IAccountsFilter filterDTO: IAccountsFilter
): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => { ): Promise<{ accounts: IAccountResponse[]; filterMeta: IFilterMeta }> => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);
const { accountRepository } = this.tenancy.repositories(tenantId);
// Parses the stringified filter roles. // Parses the stringified filter roles.
const filter = this.parseListFilterDTO(filterDTO); const filter = this.parseListFilterDTO(filterDTO);
@@ -53,17 +59,16 @@ export class GetAccounts {
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
builder.modify('inactiveMode', filter.inactiveMode); builder.modify('inactiveMode', filter.inactiveMode);
}); });
// Retrievs the formatted accounts collection.
const preTransformedAccounts = await this.transformer.transform( const accountsGraph = await accountRepository.getDependencyGraph();
// Retrieves the transformed accounts collection.
const transformedAccounts = await this.transformer.transform(
tenantId, tenantId,
accounts, accounts,
new AccountTransformer() new AccountTransformer(),
{ accountsGraph, structure: filterDTO.structure }
); );
// Transform accounts to nested array.
const transformedAccounts = flatToNestedArray(preTransformedAccounts, {
id: 'id',
parentId: 'parentAccountId',
});
return { return {
accounts: transformedAccounts, accounts: transformedAccounts,

View File

@@ -1,8 +1,14 @@
import { Service, Inject, Container } from 'typedi'; import { Service, Inject, Container } from 'typedi';
import { IRegisterDTO, ISystemUser, IPasswordReset } from '@/interfaces'; import {
IRegisterDTO,
ISystemUser,
IPasswordReset,
IAuthGetMetaPOJO,
} from '@/interfaces';
import { AuthSigninService } from './AuthSignin'; import { AuthSigninService } from './AuthSignin';
import { AuthSignupService } from './AuthSignup'; import { AuthSignupService } from './AuthSignup';
import { AuthSendResetPassword } from './AuthSendResetPassword'; import { AuthSendResetPassword } from './AuthSendResetPassword';
import { GetAuthMeta } from './GetAuthMeta';
@Service() @Service()
export default class AuthenticationApplication { export default class AuthenticationApplication {
@@ -15,6 +21,9 @@ export default class AuthenticationApplication {
@Inject() @Inject()
private authResetPasswordService: AuthSendResetPassword; private authResetPasswordService: AuthSendResetPassword;
@Inject()
private authGetMeta: GetAuthMeta;
/** /**
* Signin and generates JWT token. * Signin and generates JWT token.
* @throws {ServiceError} * @throws {ServiceError}
@@ -53,4 +62,12 @@ export default class AuthenticationApplication {
public async resetPassword(token: string, password: string): Promise<void> { public async resetPassword(token: string, password: string): Promise<void> {
return this.authResetPasswordService.resetPassword(token, password); return this.authResetPasswordService.resetPassword(token, password);
} }
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return this.authGetMeta.getAuthMeta();
}
} }

View File

@@ -1,4 +1,4 @@
import { omit } from 'lodash'; import { isEmpty, omit } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
@@ -13,6 +13,7 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import TenantsManagerService from '../Tenancy/TenantsManager'; import TenantsManagerService from '../Tenancy/TenantsManager';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { hashPassword } from '@/utils'; import { hashPassword } from '@/utils';
import config from '@/config';
export class AuthSignupService { export class AuthSignupService {
@Inject() @Inject()
@@ -33,6 +34,9 @@ export class AuthSignupService {
public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> { public async signUp(signupDTO: IRegisterDTO): Promise<ISystemUser> {
const { systemUserRepository } = this.sysRepositories; const { systemUserRepository } = this.sysRepositories;
// Validates the signup disable restrictions.
await this.validateSignupRestrictions(signupDTO.email);
// Validates the given email uniqiness. // Validates the given email uniqiness.
await this.validateEmailUniqiness(signupDTO.email); await this.validateEmailUniqiness(signupDTO.email);
@@ -74,4 +78,34 @@ export class AuthSignupService {
throw new ServiceError(ERRORS.EMAIL_EXISTS); throw new ServiceError(ERRORS.EMAIL_EXISTS);
} }
} }
/**
* Validate sign-up disable restrictions.
* @param {string} email
*/
private async validateSignupRestrictions(email: string) {
// Can't continue if the signup is not disabled.
if (!config.signupRestrictions.disabled) return;
// Validate the allowed email addresses and domains.
if (
!isEmpty(config.signupRestrictions.allowedEmails) ||
!isEmpty(config.signupRestrictions.allowedDomains)
) {
const emailDomain = email.split('@').pop();
const isAllowedEmail =
config.signupRestrictions.allowedEmails.indexOf(email) !== -1;
const isAllowedDomain = config.signupRestrictions.allowedDomains.some(
(domain) => emailDomain === domain
);
if (!isAllowedEmail && !isAllowedDomain) {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED_NOT_ALLOWED);
}
// Throw error if the signup is disabled with no exceptions.
} else {
throw new ServiceError(ERRORS.SIGNUP_RESTRICTED);
}
}
} }

View File

@@ -0,0 +1,16 @@
import { Service } from 'typedi';
import { IAuthGetMetaPOJO } from '@/interfaces';
import config from '@/config';
@Service()
export class GetAuthMeta {
/**
* Retrieves the authentication meta for SPA.
* @returns {Promise<IAuthGetMetaPOJO>}
*/
public async getAuthMeta(): Promise<IAuthGetMetaPOJO> {
return {
signupDisabled: config.signupRestrictions.disabled,
};
}
}

View File

@@ -7,4 +7,6 @@ export const ERRORS = {
TOKEN_EXPIRED: 'TOKEN_EXPIRED', TOKEN_EXPIRED: 'TOKEN_EXPIRED',
PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS', PHONE_NUMBER_EXISTS: 'PHONE_NUMBER_EXISTS',
EMAIL_EXISTS: 'EMAIL_EXISTS', EMAIL_EXISTS: 'EMAIL_EXISTS',
SIGNUP_RESTRICTED_NOT_ALLOWED: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
SIGNUP_RESTRICTED: 'SIGNUP_RESTRICTED',
}; };

View File

@@ -5,18 +5,13 @@ import {
ICreditNoteDeletedPayload, ICreditNoteDeletedPayload,
ICreditNoteEditedPayload, ICreditNoteEditedPayload,
ICreditNoteOpenedPayload, ICreditNoteOpenedPayload,
IRefundCreditNoteOpenedPayload,
} from '@/interfaces'; } from '@/interfaces';
import CreditNoteGLEntries from './CreditNoteGLEntries'; import CreditNoteGLEntries from './CreditNoteGLEntries';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class CreditNoteGLEntriesSubscriber { export default class CreditNoteGLEntriesSubscriber {
@Inject() @Inject()
creditNoteGLEntries: CreditNoteGLEntries; private creditNoteGLEntries: CreditNoteGLEntries;
@Inject()
tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -5,7 +5,7 @@ import { ICashflowAccountTransactionsQuery, IPaginationMeta } from '@/interfaces
@Service() @Service()
export default class CashflowAccountTransactionsRepo { export default class CashflowAccountTransactionsRepo {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Retrieve the cashflow account transactions. * Retrieve the cashflow account transactions.

View File

@@ -17,7 +17,7 @@ export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accural', basis: 'accrual',
noneZero: false, noneZero: false,
noneTransactions: false, noneTransactions: false,

View File

@@ -35,7 +35,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
formatMoney: 'total', formatMoney: 'total',
precision: 2, precision: 2,
}, },
basis: 'accural', basis: 'accrual',
noneZero: false, noneZero: false,
noneTransactions: true, noneTransactions: true,
onlyActive: false, onlyActive: false,

View File

@@ -12,9 +12,12 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { IAcceptInviteUserService } from '@/interfaces';
@Service() @Service()
export default class AcceptInviteUserService { export default class AcceptInviteUserService
implements IAcceptInviteUserService
{
@Inject() @Inject()
private eventPublisher: EventPublisher; private eventPublisher: EventPublisher;

View File

@@ -1,7 +1,4 @@
import { import { IUserInviteTenantSyncedEventPayload } from '@/interfaces';
IUserInvitedEventPayload,
IUserInviteTenantSyncedEventPayload,
} from '@/interfaces';
import events from '@/subscribers/events'; import events from '@/subscribers/events';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';

View File

@@ -1,12 +1,12 @@
import path from 'path';
import { ISystemUser } from '@/interfaces'; import { ISystemUser } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import Mail from '@/lib/Mail'; import Mail from '@/lib/Mail';
import { Service, Container } from 'typedi'; import { Service } from 'typedi';
import config from '@/config';
import { Tenant } from '@/system/models'; import { Tenant } from '@/system/models';
import config from '@/config';
@Service() @Service()
export default class InviteUsersMailMessages { export default class SendInviteUsersMailMessage {
/** /**
* Sends invite mail to the given email. * Sends invite mail to the given email.
* @param user * @param user
@@ -18,7 +18,7 @@ export default class InviteUsersMailMessages {
.findById(tenantId) .findById(tenantId)
.withGraphFetched('metadata'); .withGraphFetched('metadata');
const root = __dirname + '/../../../views/images/bigcapital.png'; const root = path.join(global.__views_dir, '/images/bigcapital.png');
const mail = new Mail() const mail = new Mail()
.setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`) .setSubject(`${fromUser.firstName} has invited you to join a Bigcapital`)

View File

@@ -8,7 +8,7 @@ import { IAcceptInviteEventPayload } from '@/interfaces';
@Service() @Service()
export default class SyncTenantAcceptInvite { export default class SyncTenantAcceptInvite {
@Inject() @Inject()
tenancy: HasTenancyService; private tenancy: HasTenancyService;
/** /**
* Attaches events with handlers. * Attaches events with handlers.

View File

@@ -74,17 +74,15 @@ export default class InviteTenantUserService implements IInviteUserService {
/** /**
* Re-send user invite. * Re-send user invite.
* @param {number} tenantId - * @param {number} tenantId -
* @param {string} email - * @param {string} email -
* @return {Promise<{ invite: IUserInvite }>} * @return {Promise<{ invite: IUserInvite }>}
*/ */
public async resendInvite( public async resendInvite(
tenantId: number, tenantId: number,
userId: number, userId: number,
authorizedUser: ISystemUser authorizedUser: ISystemUser
): Promise<{ ): Promise<{ user: ITenantUser }> {
user: ITenantUser;
}> {
// Retrieve the user by id or throw not found service error. // Retrieve the user by id or throw not found service error.
const user = await this.getUserByIdOrThrowError(tenantId, userId); const user = await this.getUserByIdOrThrowError(tenantId, userId);

View File

@@ -1,11 +1,10 @@
import { difference, sumBy, omit, map } from 'lodash'; import { difference } from 'lodash';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import {
IManualJournalDTO, IManualJournalDTO,
IManualJournalEntry, IManualJournalEntry,
IManualJournal, IManualJournal,
IManualJournalEntryDTO,
} from '@/interfaces'; } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
@@ -286,7 +285,7 @@ export class CommandManualJournalValidators {
public validateJournalCurrencyWithAccountsCurrency = async ( public validateJournalCurrencyWithAccountsCurrency = async (
tenantId: number, tenantId: number,
manualJournalDTO: IManualJournalDTO, manualJournalDTO: IManualJournalDTO,
baseCurrency: string, baseCurrency: string
) => { ) => {
const { Account } = this.tenancy.models(tenantId); const { Account } = this.tenancy.models(tenantId);

View File

@@ -3,25 +3,20 @@ import * as R from 'ramda';
import { import {
IManualJournal, IManualJournal,
IManualJournalEntry, IManualJournalEntry,
IAccount,
ILedgerEntry, ILedgerEntry,
} from '@/interfaces'; } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import Ledger from '@/services/Accounting/Ledger'; import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { LedgerRevert } from '@/services/Accounting/LedgerStorageRevert';
@Service() @Service()
export class ManualJournalGLEntries { export class ManualJournalGLEntries {
@Inject() @Inject()
ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
@Inject() @Inject()
ledgerRevert: LedgerRevert; private tenancy: HasTenancyService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Create manual journal GL entries. * Create manual journal GL entries.
@@ -77,7 +72,7 @@ export class ManualJournalGLEntries {
manualJournalId: number, manualJournalId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<void> => { ): Promise<void> => {
return this.ledgerRevert.revertGLEntries( return this.ledgerStorage.deleteByReference(
tenantId, tenantId,
manualJournalId, manualJournalId,
'Journal', 'Journal',
@@ -86,7 +81,7 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the ledger of the given manual journal.
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {Ledger} * @returns {Ledger}
*/ */
@@ -97,11 +92,13 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the common entry details of the manual journal
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {} * @returns {Partial<ILedgerEntry>}
*/ */
private getManualJournalCommonEntry = (manualJournal: IManualJournal) => { private getManualJournalCommonEntry = (
manualJournal: IManualJournal
): Partial<ILedgerEntry> => {
return { return {
transactionNumber: manualJournal.journalNumber, transactionNumber: manualJournal.journalNumber,
referenceNumber: manualJournal.reference, referenceNumber: manualJournal.reference,
@@ -118,7 +115,8 @@ export class ManualJournalGLEntries {
}; };
/** /**
* * Retrieves the ledger entry of the given manual journal and
* its associated entry.
* @param {IManualJournal} manualJournal - * @param {IManualJournal} manualJournal -
* @param {IManualJournalEntry} entry - * @param {IManualJournalEntry} entry -
* @returns {ILedgerEntry} * @returns {ILedgerEntry}
@@ -149,7 +147,7 @@ export class ManualJournalGLEntries {
); );
/** /**
* * Retrieves the ledger of the given manual journal.
* @param {IManualJournal} manualJournal * @param {IManualJournal} manualJournal
* @returns {ILedgerEntry[]} * @returns {ILedgerEntry[]}
*/ */

View File

@@ -23,8 +23,11 @@ export class ProjectBillableBillSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableBill this.handleIncreaseBillableBill
); );
bus.subscribe(events.saleInvoice.onEdited, this.handleDecreaseBillableBill); bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableBill);
bus.subscribe(events.saleInvoice.onDeleted, this.handleEditBillableBill); bus.subscribe(
events.saleInvoice.onDeleted,
this.handleDecreaseBillableBill
);
} }
/** /**

View File

@@ -1,7 +1,11 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import async from 'async'; import async from 'async';
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces'; import {
ISaleInvoice,
ISaleInvoiceDTO,
ProjectLinkRefType,
} from '@/interfaces';
import { ProjectBillableExpense } from './ProjectBillableExpense'; import { ProjectBillableExpense } from './ProjectBillableExpense';
import { filterEntriesByRefType } from './_utils'; import { filterEntriesByRefType } from './_utils';

View File

@@ -21,13 +21,10 @@ export class ProjectBillableExpensesSubscriber {
events.saleInvoice.onCreated, events.saleInvoice.onCreated,
this.handleIncreaseBillableExpenses this.handleIncreaseBillableExpenses
); );
bus.subscribe( bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableExpenses);
events.saleInvoice.onEdited,
this.handleDecreaseBillableExpenses
);
bus.subscribe( bus.subscribe(
events.saleInvoice.onDeleted, events.saleInvoice.onDeleted,
this.handleEditBillableExpenses this.handleDecreaseBillableExpenses
); );
} }

View File

@@ -419,6 +419,58 @@ export const parseDate = (date: string) => {
return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : ''; return date ? moment(date).utcOffset(0).format('YYYY-MM-DD') : '';
}; };
const nestedArrayToFlatten = (
collection,
property = 'children',
parseItem = (a, level) => a,
level = 1
) => {
const parseObject = (obj) =>
parseItem(
{
..._.omit(obj, [property]),
},
level
);
return collection.reduce((items, currentValue, index) => {
let localItems = [...items];
const parsedItem = parseObject(currentValue, level);
localItems.push(parsedItem);
if (Array.isArray(currentValue[property])) {
const flattenArray = nestedArrayToFlatten(
currentValue[property],
property,
parseItem,
level + 1
);
localItems = _.concat(localItems, flattenArray);
}
return localItems;
}, []);
};
const assocDepthLevelToObjectTree = (
objects,
level = 1,
propertyName = 'level'
) => {
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
object[propertyName] = level;
if (object.children) {
assocDepthLevelToObjectTree(object.children, level + 1, propertyName);
}
}
return objects;
};
const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
return envVar ? envVar?.split(',')?.map(_.trim) : [];
};
export { export {
templateRender, templateRender,
accumSum, accumSum,
@@ -449,4 +501,7 @@ export {
dateRangeFromToCollection, dateRangeFromToCollection,
transformToMapKeyValue, transformToMapKeyValue,
mergeObjectsBykey, mergeObjectsBykey,
nestedArrayToFlatten,
assocDepthLevelToObjectTree,
castCommaListEnvVarToArray
}; };

View File

@@ -5,10 +5,10 @@ USER root
WORKDIR /app WORKDIR /app
# Install dependencies # Install dependencies
COPY package.json ./ COPY package*.json ./
COPY lerna.json ./ COPY lerna.json ./
COPY ./packages/webapp/package.json /app/packages/webapp/package.json COPY ./packages/webapp/package*.json /app/packages/webapp/
RUN npm install RUN npm install
RUN npm run bootstrap RUN npm run bootstrap

View File

@@ -1205,9 +1205,9 @@
} }
}, },
"@blueprintjs-formik/core": { "@blueprintjs-formik/core": {
"version": "0.2.1", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.2.1.tgz", "resolved": "https://registry.npmjs.org/@blueprintjs-formik/core/-/core-0.3.3.tgz",
"integrity": "sha512-YGJe+QorDGbkWDSUg6x69LYGN62Kgvb92Iz/voqmszVRKj4KcoPvd/7coF8Jmu+ZQE6LcwM/9ccB2i63L99ITA==", "integrity": "sha512-ko7g54YSEcSq2K/GEpmiTG0foGLqe7DwgXGhkGxYEiHhLAUv8WvQmrFsm8e/KOW7n8mLGq0uaZVe2l8m3JTGGQ==",
"requires": { "requires": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.keyby": "^4.6.0", "lodash.keyby": "^4.6.0",
@@ -1227,9 +1227,9 @@
} }
}, },
"@blueprintjs-formik/select": { "@blueprintjs-formik/select": {
"version": "0.1.5", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.1.5.tgz", "resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.2.3.tgz",
"integrity": "sha512-EqGbuoiS1VrWpzjd39uVhBAmfVobdpgqalGcpODyGA+XAYoft1UU12yzTzrEOwBZpQKiC12UQwekUPspYBsVKA==", "integrity": "sha512-j/zkX0B9wgtoHgK6Z/rlowB7F7zemrAajBU+d3caCoEYMMqwAI0XA++GytqrIhv5fEGjkZ1hkxS9j8eqX8vtjA==",
"requires": { "requires": {
"lodash.get": "^4.4.2", "lodash.get": "^4.4.2",
"lodash.keyby": "^4.6.0", "lodash.keyby": "^4.6.0",
@@ -7298,6 +7298,11 @@
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
}, },
"flat": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
},
"flat-cache": { "flat-cache": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",

View File

@@ -3,9 +3,9 @@
"version": "1.7.1", "version": "1.7.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@blueprintjs-formik/core": "^0.2.1", "@blueprintjs-formik/core": "^0.3.3",
"@blueprintjs-formik/datetime": "^0.3.4", "@blueprintjs-formik/datetime": "^0.3.4",
"@blueprintjs-formik/select": "^0.1.4", "@blueprintjs-formik/select": "^0.2.3",
"@blueprintjs/core": "^3.50.2", "@blueprintjs/core": "^3.50.2",
"@blueprintjs/datetime": "^3.23.12", "@blueprintjs/datetime": "^3.23.12",
"@blueprintjs/popover2": "^0.11.1", "@blueprintjs/popover2": "^0.11.1",
@@ -16,8 +16,6 @@
"@casl/react": "^2.3.0", "@casl/react": "^2.3.0",
"@craco/craco": "^5.9.0", "@craco/craco": "^5.9.0",
"@reduxjs/toolkit": "^1.2.5", "@reduxjs/toolkit": "^1.2.5",
"@sentry/react": "^6.13.2",
"@sentry/tracing": "^6.13.2",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.4.0", "@testing-library/react": "^9.4.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
@@ -45,6 +43,7 @@
"deepdash": "^5.3.9", "deepdash": "^5.3.9",
"dependency-graph": "^0.11.0", "dependency-graph": "^0.11.0",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"flat": "^5.0.2",
"formik": "^2.2.5", "formik": "^2.2.5",
"http-proxy-middleware": "^1.0.0", "http-proxy-middleware": "^1.0.0",
"jest": "24.9.0", "jest": "24.9.0",

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

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