mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-17 05:10:31 +00:00
add server to monorepo.
This commit is contained in:
8
packages/server/.babelrc
Normal file
8
packages/server/.babelrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"retainLines": true,
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
"@babel/plugin-syntax-dynamic-import"
|
||||
]
|
||||
}
|
||||
41
packages/server/.env.example
Normal file
41
packages/server/.env.example
Normal file
@@ -0,0 +1,41 @@
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_USERNAME=842f331d3dc005
|
||||
MAIL_PASSWORD=172f97b34f1a17
|
||||
MAIL_PORT=587
|
||||
MAIL_SECURE=false
|
||||
MAIL_FROM_NAME=Bigcapital
|
||||
MAIL_FROM_ADDRESS=noreply@sender.bigcapital.ly
|
||||
|
||||
SYSTEM_DB_CLIENT=mysql
|
||||
SYSTEM_DB_HOST=127.0.0.1
|
||||
SYSTEM_DB_USER=root
|
||||
SYSTEM_DB_PASSWORD=root
|
||||
SYSTEM_DB_NAME=bigcapital_system
|
||||
SYSTEM_MIGRATIONS_DIR=./src/system/migrations
|
||||
SYSTEM_SEEDS_DIR=./src/system/seeds
|
||||
|
||||
TENANT_DB_CLIENT=mysql
|
||||
TENANT_DB_NAME_PERFIX=bigcapital_tenant_
|
||||
TENANT_DB_HOST=127.0.0.1
|
||||
TENANT_DB_PASSWORD=root
|
||||
TENANT_DB_USER=root
|
||||
TENANT_DB_CHARSET=utf8
|
||||
TENANT_MIGRATIONS_DIR=src/database/migrations
|
||||
TENANT_SEEDS_DIR=src/database/seeds/core
|
||||
|
||||
DB_MANAGER_SUPER_USER=root
|
||||
DB_MANAGER_SUPER_PASSWORD=root
|
||||
|
||||
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
|
||||
|
||||
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
|
||||
|
||||
CONTACT_US_MAIL=support@bigcapital.ly
|
||||
BASE_URL=https://bigcapital.ly
|
||||
|
||||
LICENSES_AUTH_USER=root
|
||||
LICENSES_AUTH_PASSWORD=root
|
||||
|
||||
AGENDASH_AUTH_USER=agendash
|
||||
AGENDASH_AUTH_PASSWORD=123123
|
||||
BROWSER_WS_ENDPOINT=ws://localhost:4080/
|
||||
34
packages/server/.eslintrc.js
Normal file
34
packages/server/.eslintrc.js
Normal file
@@ -0,0 +1,34 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
extends: ['airbnb-base', 'airbnb-typescript'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: './',
|
||||
},
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly',
|
||||
},
|
||||
plugins: ['import'],
|
||||
rules: {
|
||||
'import/no-unresolved': 'error',
|
||||
'import/prefer-default-export': 'off',
|
||||
},
|
||||
settings: {
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx'],
|
||||
},
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
project: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
7
packages/server/.gitignore
vendored
Normal file
7
packages/server/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/storage
|
||||
package-lock.json
|
||||
stdout.log
|
||||
/dist
|
||||
/build
|
||||
76
packages/server/CHANGELOG.md
Normal file
76
packages/server/CHANGELOG.md
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
# Change Log
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [1.7.1-rc.2] - 30-03-2022
|
||||
|
||||
### Fixed
|
||||
- `BIG-354` Validate the warehouse transfer quantity should be above zero.
|
||||
- `BIG-358` Refactoring customers/vendors services for smaller classes.
|
||||
- `BIG-341` Refactoring expenses services for smaller classes.
|
||||
- `BIG-342` Assign default currency as base currency when create customer, vendor or expense transaction.
|
||||
## [1.7.0-rc.1] - 24-03-2022
|
||||
|
||||
## Added
|
||||
- Multiply currencies with foreign currencies.
|
||||
- Multiply warehouses to track inventory items.
|
||||
- Multiply branches to track organization transactions.
|
||||
- Transfer orders between warehouses.
|
||||
- Integrate financial reports with multiply branches.
|
||||
- Integrate inventory reports with multiply warehouses.
|
||||
|
||||
## [1.6.1] - 19-02-2022
|
||||
### Fixed
|
||||
- fix: `BIG-329` Total of aggregate/accounts nodes of Balance sheet.
|
||||
- fix: `BIG-329` Total of aggregate/accounts nodes of Profit & Loss sheet.
|
||||
- fix: `BIG-328` Localization of total column label of P&L sheet date periods mode.
|
||||
|
||||
## [1.6.0] - 18-02-2022
|
||||
|
||||
### Added
|
||||
- Balance sheet comparison of previous period (PP).
|
||||
- Balance sheet comparison of previous year (PY).
|
||||
- Balance sheet percentage analysis columns and rows basis.
|
||||
- Profit & loss sheet comparison of preivous period (PP).
|
||||
- Profit & loss sheet comparison of previous year (PY).
|
||||
- Profit & loss sheet percentage analysis columns, rows, income and expenses basis.
|
||||
|
||||
|
||||
## [1.5.3] - 13-01-2022
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimize style of sale invoice PDF template.
|
||||
- Optimize style of sale estimate PDF template.
|
||||
- Optimize style of credit note PDF template.
|
||||
- Optimize style of payment receive PDF template.
|
||||
- Optimize style of sale receipt PDF template.
|
||||
|
||||
### Fixed
|
||||
- fix: Customer and vendor balance summary percentage of column.
|
||||
- fix: Filtering none transactions and zero accounts of cashflow sheet.
|
||||
|
||||
## [Unreleased] - yyyy-mm-dd
|
||||
|
||||
Here we write upgrading notes for brands. It's a team effort to make them as
|
||||
straightforward as possible.
|
||||
|
||||
### Added
|
||||
- Dynamic filter for resources list.
|
||||
- Dynamic search for resources list.
|
||||
- Dynamic resources to switch between active and inactive items.
|
||||
- Add virtual computed attributes to sale invoice list and individual.
|
||||
- This CHANGELOG file to hopefully serve as an evolving example of a
|
||||
standardized open source project CHANGELOG.
|
||||
- Remove subscription free trial.
|
||||
|
||||
### Changed
|
||||
- Redesigne organization tenant metadata table to depend on table on system
|
||||
database instead of tenant database.
|
||||
-
|
||||
|
||||
### Fixed
|
||||
|
||||
0
packages/server/README.md
Normal file
0
packages/server/README.md
Normal file
203
packages/server/bin/bigcapital.js
Normal file
203
packages/server/bin/bigcapital.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import commander from 'commander';
|
||||
import color from 'colorette';
|
||||
import argv from 'getopts'
|
||||
import config from '../src/config';
|
||||
import {
|
||||
initSystemKnex,
|
||||
getAllSystemTenants,
|
||||
initTenantKnex,
|
||||
exit,
|
||||
success,
|
||||
log,
|
||||
} from './utils';
|
||||
|
||||
// - bigcapital system:migrate:latest
|
||||
// - bigcapital system:migrate:rollback
|
||||
// - bigcapital tenants:migrate:latest
|
||||
// - bigcapital tenants:migrate:latest --tenant_id=XXX
|
||||
// - bigcapital tenants:migrate:rollback
|
||||
// - bigcapital tenants:migrate:rollback --tenant_id=XXX
|
||||
// - bigcapital tenants:migrate:make
|
||||
// - bigcapital system:migrate:make
|
||||
// - bigcapital tenants:list
|
||||
|
||||
commander
|
||||
.command('system:migrate:rollback')
|
||||
.description('Migrate the system database of the application.')
|
||||
.action(async () => {
|
||||
try {
|
||||
const sysKnex = await initSystemKnex();
|
||||
const [batchNo, _log] = await sysKnex.migrate.rollback();
|
||||
|
||||
if (_log.length === 0) {
|
||||
success(color.cyan('Already at the base migration'));
|
||||
}
|
||||
success(
|
||||
color.green(
|
||||
`Batch ${batchNo} rolled back: ${_log.length} migrations`
|
||||
) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
|
||||
);
|
||||
} catch(error) {
|
||||
exit(error);
|
||||
}
|
||||
});
|
||||
|
||||
commander
|
||||
.command('system:migrate:latest')
|
||||
.description('Rollback latest mgiration of the system database.')
|
||||
.action(async () => {
|
||||
try {
|
||||
const sysKnex = await initSystemKnex();
|
||||
const [batchNo, log] = await sysKnex.migrate.latest();
|
||||
|
||||
if (log.length === 0) {
|
||||
success(color.cyan('Already up to date'));
|
||||
}
|
||||
success(
|
||||
color.green(`Batch ${batchNo} run: ${log.length} migrations`) +
|
||||
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
|
||||
);
|
||||
} catch(error) {
|
||||
exit(error);
|
||||
}
|
||||
});
|
||||
|
||||
commander
|
||||
.command('system:migrate:make <name>')
|
||||
.description('Created a named migration file to the system database.')
|
||||
.action(async (name) => {
|
||||
const sysKnex = await initSystemKnex();
|
||||
|
||||
sysKnex.migrate.make(name).then((name) => {
|
||||
success(color.green(`Created Migration: ${name}`));
|
||||
}).catch(exit)
|
||||
});
|
||||
|
||||
commander
|
||||
.command('tenants:migrate:make <name>')
|
||||
.description('Created a name migration file to the tenants databases.')
|
||||
.action(async (name) => {
|
||||
const sysKnex = await initTenantKnex();
|
||||
|
||||
sysKnex.migrate.make(name).then((name) => {
|
||||
success(color.green(`Created Migration: ${name}`));
|
||||
}).catch(exit)
|
||||
});
|
||||
|
||||
commander
|
||||
.command('tenants:list')
|
||||
.description('Retrieve a list of all system tenants databases.')
|
||||
.action(async (cmd) => {
|
||||
try{
|
||||
const sysKnex = await initSystemKnex();
|
||||
const tenants = await getAllSystemTenants(sysKnex);
|
||||
|
||||
tenants.forEach((tenant) => {
|
||||
const dbName = `${config.tenant.db_name_prefix}${tenant.organizationId}`;
|
||||
console.log(`ID: ${tenant.id} | Organization ID: ${tenant.organizationId} | DB Name: ${dbName}`);
|
||||
});
|
||||
} catch(error) { exit(error); };
|
||||
success('---');
|
||||
});
|
||||
|
||||
commander
|
||||
.command('tenants:migrate:rollback')
|
||||
.description('Rollback the last batch of tenants migrations.')
|
||||
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
|
||||
.action(async (cmd) => {
|
||||
try {
|
||||
const sysKnex = await initSystemKnex();
|
||||
const tenants = await getAllSystemTenants(sysKnex);
|
||||
const tenantsOrgsIds = tenants.map((tenant) => tenant.organizationId);
|
||||
|
||||
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
|
||||
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
|
||||
}
|
||||
|
||||
const migrateOpers = [];
|
||||
const migrateTenant = async (organizationId) => {
|
||||
try {
|
||||
const tenantKnex = await initTenantKnex(organizationId);
|
||||
const [batchNo, _log] = await tenantKnex.migrate.rollback();
|
||||
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
|
||||
|
||||
if (_log.length === 0) {
|
||||
log(color.cyan('Already at the base migration'));
|
||||
}
|
||||
log(
|
||||
color.green(
|
||||
`Tenant: ${tenantDb} > Batch ${batchNo} rolled back: ${_log.length} migrations`
|
||||
) + (argv.verbose ? `\n${color.cyan(_log.join('\n'))}` : '')
|
||||
);
|
||||
log('---------------');
|
||||
} catch (error) { exit(error); }
|
||||
};
|
||||
|
||||
if (!cmd.tenant_id) {
|
||||
tenants.forEach((tenant) => {
|
||||
const oper = migrateTenant(tenant.organizationId);
|
||||
migrateOpers.push(oper);
|
||||
});
|
||||
} else {
|
||||
const oper = migrateTenant(cmd.tenant_id);
|
||||
migrateOpers.push(oper);
|
||||
}
|
||||
Promise.all(migrateOpers).then(() => {
|
||||
success('All tenants are rollbacked.');
|
||||
});
|
||||
} catch (error) { exit(error); }
|
||||
});
|
||||
|
||||
commander
|
||||
.command('tenants:migrate:latest')
|
||||
.description('Migrate all tenants or the given tenant id.')
|
||||
.option('-t, --tenant_id [tenant_id]', 'Which tenant id do you migrate.')
|
||||
.action(async (cmd) => {
|
||||
try {
|
||||
const sysKnex = await initSystemKnex();
|
||||
const tenants = await getAllSystemTenants(sysKnex);
|
||||
const tenantsOrgsIds = tenants.map(tenant => tenant.organizationId);
|
||||
|
||||
if (cmd.tenant_id && tenantsOrgsIds.indexOf(cmd.tenant_id) === -1) {
|
||||
exit(`The given tenant id ${cmd.tenant_id} is not exists.`);
|
||||
}
|
||||
// Validate the tenant id exist first of all.
|
||||
const migrateOpers = [];
|
||||
const migrateTenant = async (organizationId) => {
|
||||
try {
|
||||
const tenantKnex = await initTenantKnex(organizationId);
|
||||
const [batchNo, _log] = await tenantKnex.migrate.latest();
|
||||
|
||||
const tenantDb = `${config.tenant.db_name_prefix}${organizationId}`;
|
||||
|
||||
if (_log.length === 0) {
|
||||
log(color.cyan('Already up to date'));
|
||||
}
|
||||
log(
|
||||
color.green(`Tenant ${tenantDb} > Batch ${batchNo} run: ${_log.length} migrations`) +
|
||||
(argv.verbose ? `\n${color.cyan(log.join('\n'))}` : '')
|
||||
);
|
||||
log('-------------------');
|
||||
} catch (error) {
|
||||
log(error);
|
||||
}
|
||||
}
|
||||
if (!cmd.tenant_id) {
|
||||
tenants.forEach((tenant) => {
|
||||
const oper = migrateTenant(tenant.organizationId);
|
||||
migrateOpers.push(oper);
|
||||
});
|
||||
} else {
|
||||
const oper = migrateTenant(cmd.tenant_id);
|
||||
migrateOpers.push(oper);
|
||||
}
|
||||
|
||||
Promise.all(migrateOpers).then(() => {
|
||||
success('All tenants are migrated.');
|
||||
});
|
||||
} catch (error) {
|
||||
exit(error);
|
||||
}
|
||||
});
|
||||
|
||||
commander.parse(process.argv);
|
||||
94
packages/server/bin/utils.js
Normal file
94
packages/server/bin/utils.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import Knex from 'knex';
|
||||
import { knexSnakeCaseMappers } from 'objection';
|
||||
import color from 'colorette';
|
||||
import config from '../src/config';
|
||||
// import { systemKnexConfig } from '../src/config/knexConfig';
|
||||
|
||||
function initSystemKnex() {
|
||||
return Knex({
|
||||
client: config.system.db_client,
|
||||
connection: {
|
||||
host: config.system.db_host,
|
||||
user: config.system.db_user,
|
||||
password: config.system.db_password,
|
||||
database: config.system.db_name,
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: config.system.migrations_dir,
|
||||
},
|
||||
seeds: {
|
||||
directory: config.system.seeds_dir,
|
||||
},
|
||||
pool: { min: 0, max: 7 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
});
|
||||
}
|
||||
|
||||
function getAllSystemTenants(knex) {
|
||||
return knex('tenants');
|
||||
}
|
||||
|
||||
function initTenantKnex(organizationId) {
|
||||
return Knex({
|
||||
client: config.tenant.db_client,
|
||||
connection: {
|
||||
host: config.tenant.db_host,
|
||||
user: config.tenant.db_user,
|
||||
password: config.tenant.db_password,
|
||||
database: `${config.tenant.db_name_prefix}${organizationId}`,
|
||||
charset: config.tenant.charset,
|
||||
},
|
||||
migrations: {
|
||||
directory: config.tenant.migrations_dir,
|
||||
},
|
||||
seeds: {
|
||||
directory: config.tenant.seeds_dir,
|
||||
},
|
||||
pool: { min: 0, max: 5 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
})
|
||||
}
|
||||
|
||||
function exit(text) {
|
||||
if (text instanceof Error) {
|
||||
console.error(
|
||||
color.red(`${text.detail ? `${text.detail}\n` : ''}${text.stack}`)
|
||||
);
|
||||
} else {
|
||||
console.error(color.red(text));
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function success(text) {
|
||||
console.log(text);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function log(text) {
|
||||
console.log(text);
|
||||
}
|
||||
|
||||
function getDeepValue(prop, obj) {
|
||||
if (!Object.keys(obj).length) { return []; }
|
||||
|
||||
return Object.entries(obj).reduce((acc, [key, val]) => {
|
||||
if (key === prop) {
|
||||
acc.push(val);
|
||||
} else {
|
||||
acc.push(Array.isArray(val) ? val.map(getIds).flat() : getIds(val));
|
||||
}
|
||||
return acc.flat();
|
||||
}, []);
|
||||
}
|
||||
|
||||
export {
|
||||
initTenantKnex,
|
||||
initSystemKnex,
|
||||
getAllSystemTenants,
|
||||
exit,
|
||||
success,
|
||||
log,
|
||||
getDeepValue,
|
||||
}
|
||||
17
packages/server/knexfile.js
Normal file
17
packages/server/knexfile.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { knexSnakeCaseMappers } = require('objection');
|
||||
|
||||
module.exports = {
|
||||
client: 'mysql',
|
||||
connection: {
|
||||
host: '127.0.0.1',
|
||||
user: 'root',
|
||||
password: 'root',
|
||||
database: 'bigcapital_tenant_hqde5zqkylsho06',
|
||||
charset: 'utf8',
|
||||
},
|
||||
migrations: {
|
||||
directory: './src/database/migrations',
|
||||
},
|
||||
pool: { min: 0, max: 7 },
|
||||
...knexSnakeCaseMappers({ upperCase: true }),
|
||||
};
|
||||
144
packages/server/package.json
Normal file
144
packages/server/package.json
Normal file
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"name": "bigcapital-server",
|
||||
"version": "1.7.1",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
"scripts": {
|
||||
"inspect": "cross-env NODE_PATH=./src nodemon src/server.ts",
|
||||
"clear": "rimraf build",
|
||||
"watch": "cross-env NODE_ENV=development webpack --config scripts/webpack.config.js",
|
||||
"build:resources": "gulp --gulpfile=scripts/gulpfile.js styles styles-rtl",
|
||||
"build": "cross-env NODE_ENV=production webpack --config scripts/webpack.config.js",
|
||||
"lint:fix": "eslint --fix ./**/*.ts"
|
||||
},
|
||||
"author": "Ahmed Bouhuolia, <a.bouhuolia@gmail.com>",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"bigcapital": "./bin/bigcapital.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@hapi/boom": "^7.4.3",
|
||||
"@types/i18n": "^0.8.7",
|
||||
"@types/knex": "^0.16.1",
|
||||
"@types/mathjs": "^6.0.12",
|
||||
"accepts": "^1.3.7",
|
||||
"accounting": "^0.4.1",
|
||||
"agenda": "^4.2.1",
|
||||
"agendash": "^3.1.0",
|
||||
"app-root-path": "^3.0.0",
|
||||
"async": "^3.2.0",
|
||||
"axios": "^0.20.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bluebird": "^3.7.2",
|
||||
"compression": "^1.7.4",
|
||||
"country-codes-list": "^1.6.8",
|
||||
"cpy": "^8.1.2",
|
||||
"cpy-cli": "^3.1.1",
|
||||
"crypto-random-string": "^3.2.0",
|
||||
"csurf": "^1.10.0",
|
||||
"deep-map": "^2.0.0",
|
||||
"deepdash": "^5.3.7",
|
||||
"dotenv": "^8.1.0",
|
||||
"errorhandler": "^1.5.1",
|
||||
"es6-weak-map": "^2.0.3",
|
||||
"esm": "^3.2.25",
|
||||
"event-dispatch": "^0.4.1",
|
||||
"eventemitter2": "^6.4.5",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-boom": "^3.0.0",
|
||||
"express-fileupload": "^1.1.7-alpha.3",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.12.2",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-sass": "^5.0.0",
|
||||
"helmet": "^3.21.0",
|
||||
"i18n": "^0.13.3",
|
||||
"is-my-json-valid": "^2.20.5",
|
||||
"js-money": "^0.6.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"knex": "^0.95.15",
|
||||
"knex-cleaner": "^1.3.0",
|
||||
"knex-db-manager": "^0.6.1",
|
||||
"libphonenumber-js": "^1.9.6",
|
||||
"lodash": "^4.17.15",
|
||||
"lru-cache": "^6.0.0",
|
||||
"mathjs": "^9.4.0",
|
||||
"memory-cache": "^0.2.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-range": "^4.0.2",
|
||||
"mongoose": "^5.10.0",
|
||||
"mustache": "^3.0.3",
|
||||
"mysql": "^2.17.1",
|
||||
"mysql2": "^1.6.5",
|
||||
"node-cache": "^4.2.1",
|
||||
"nodemailer": "^6.3.0",
|
||||
"nodemon": "^1.19.1",
|
||||
"object-hash": "^2.0.3",
|
||||
"objection": "^3.0.0",
|
||||
"objection-filter": "^4.0.1",
|
||||
"objection-soft-delete": "^1.0.7",
|
||||
"objection-unique": "^1.2.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"pug": "^3.0.2",
|
||||
"puppeteer": "^10.2.0",
|
||||
"qim": "0.0.52",
|
||||
"ramda": "^0.27.1",
|
||||
"rate-limiter-flexible": "^2.1.14",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rtl-detect": "^1.0.4",
|
||||
"ts-transformer-keys": "^0.4.2",
|
||||
"tsyringe": "^4.3.0",
|
||||
"typedi": "^0.8.0",
|
||||
"uniqid": "^5.2.0",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.158",
|
||||
"@types/ramda": "^0.27.64",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-http": "^4.3.0",
|
||||
"chai-things": "^0.2.0",
|
||||
"colorette": "^1.2.0",
|
||||
"commander": "^5.0.0",
|
||||
"cross-env": "^5.2.0",
|
||||
"eslint": "^8.33.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-import-resolver-typescript": "^3.5.3",
|
||||
"eslint-import-resolver-webpack": "^0.11.1",
|
||||
"eslint-loader": "^2.2.1",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"faker": "^4.1.0",
|
||||
"getopts": "^2.2.5",
|
||||
"gulp-postcss": "^9.0.0",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"knex-factory": "0.0.6",
|
||||
"merge-stream": "^2.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nyc": "^14.1.1",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"regenerator-runtime": "^0.13.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"rtlcss": "^3.3.0",
|
||||
"run-script-webpack-plugin": "^0.1.1",
|
||||
"sass": "^1.37.5",
|
||||
"sinon": "^7.4.2",
|
||||
"start-server-webpack-plugin": "^2.2.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^9.0.0",
|
||||
"tsconfig-paths-webpack-plugin": "^4.0.0",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-merge": "^5.8.0",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"webpack-watch-changed": "^1.0.0"
|
||||
}
|
||||
}
|
||||
553
packages/server/resources/css/modules/credit-rtl.css
Normal file
553
packages/server/resources/css/modules/credit-rtl.css
Normal file
File diff suppressed because one or more lines are too long
553
packages/server/resources/css/modules/credit.css
Normal file
553
packages/server/resources/css/modules/credit.css
Normal file
File diff suppressed because one or more lines are too long
544
packages/server/resources/css/modules/estimate-rtl.css
Normal file
544
packages/server/resources/css/modules/estimate-rtl.css
Normal file
File diff suppressed because one or more lines are too long
544
packages/server/resources/css/modules/estimate.css
Normal file
544
packages/server/resources/css/modules/estimate.css
Normal file
File diff suppressed because one or more lines are too long
553
packages/server/resources/css/modules/invoice-rtl.css
Normal file
553
packages/server/resources/css/modules/invoice-rtl.css
Normal file
File diff suppressed because one or more lines are too long
553
packages/server/resources/css/modules/invoice.css
Normal file
553
packages/server/resources/css/modules/invoice.css
Normal file
File diff suppressed because one or more lines are too long
553
packages/server/resources/css/modules/payment-rtl.css
Normal file
553
packages/server/resources/css/modules/payment-rtl.css
Normal file
File diff suppressed because one or more lines are too long
553
packages/server/resources/css/modules/payment.css
Normal file
553
packages/server/resources/css/modules/payment.css
Normal file
File diff suppressed because one or more lines are too long
546
packages/server/resources/css/modules/receipt-rtl.css
Normal file
546
packages/server/resources/css/modules/receipt-rtl.css
Normal file
File diff suppressed because one or more lines are too long
546
packages/server/resources/css/modules/receipt.css
Normal file
546
packages/server/resources/css/modules/receipt.css
Normal file
File diff suppressed because one or more lines are too long
640
packages/server/resources/locales/ar.json
Normal file
640
packages/server/resources/locales/ar.json
Normal file
@@ -0,0 +1,640 @@
|
||||
{
|
||||
"Petty Cash": "العهدة",
|
||||
"Cash": "النقدية",
|
||||
"Bank": "المصرف",
|
||||
"Other Income": "إيرادات اخري",
|
||||
"Interest Income": "إيرادات الفوائد",
|
||||
"Depreciation Expense": "مصاريف الاهلاك",
|
||||
"Interest Expense": "مصروفات الفوائد",
|
||||
"Sales of Product Income": "مبيعات دخل المنتجات",
|
||||
"Inventory Asset": "المخزون",
|
||||
"Cost of Goods Sold (COGS)": "تكلفة البضائع المباعة (COGS)",
|
||||
"Cost of Goods Sold": "تكلفة البضاعة المباعة",
|
||||
"Accounts Payable": "الذمم الدائنة",
|
||||
"Other Expense": "مصاريف أخرى",
|
||||
"Payroll Expenses": "مصاريف المرتبات",
|
||||
"Fixed Asset": "أصول ثابتة",
|
||||
"Credit Card": "بطاقة إئتمان",
|
||||
"Non-Current Asset": "أصول غير متداولة",
|
||||
"Current Asset": "أصول متداولة",
|
||||
"Other Asset": "أصول اخري",
|
||||
"Long Term Liability": "التزامات طويلة الاجل",
|
||||
"Current Liability": "التزامات قصيرة الاجل",
|
||||
"Other Liability": "التزمات اخري",
|
||||
"Equity": "حقوق الملكية",
|
||||
"Expense": "مصروف",
|
||||
"Income": "إيراد",
|
||||
"Accounts Receivable (A/R)": "الذمم المدينة",
|
||||
"Accounts Receivable": "الذمم المدينة",
|
||||
"Accounts Payable (A/P)": "الذمم الدائنة",
|
||||
"Inactive": "غير نشط",
|
||||
"Other Current Asset": "أصول متداولة اخرى",
|
||||
"Tax Payable": "الضريبة المستحقة",
|
||||
"Other Current Liability": "التزامات قصيرة الأجر اخرى",
|
||||
"Non-Current Liability": "التزامات طويلة الأجر",
|
||||
"Assets": "أصول",
|
||||
"Liabilities": "الالتزمات",
|
||||
"Account name": "أسم الحساب",
|
||||
"Account type": "نوع الحساب",
|
||||
"Account normal": "حساب عادي",
|
||||
"Description": "وصف",
|
||||
"Account code": "رمز الحساب",
|
||||
"Currency": "عملة",
|
||||
"Balance": "توازن",
|
||||
"Active": "نشيط",
|
||||
"Created at": "أنشئت في",
|
||||
"fixed_asset": "أصل ثابت",
|
||||
"Journal": "قيد",
|
||||
"Reconciliation": "تسوية",
|
||||
"Credit": "دائن",
|
||||
"Debit": "مدين",
|
||||
"Interest": "فائدة",
|
||||
"Depreciation": "اهلاك",
|
||||
"Payroll": "كشف رواتب",
|
||||
"Type": "نوع",
|
||||
"Name": "الأسم",
|
||||
"Sellable": "قابل للبيع",
|
||||
"Purchasable": "قابل للشراء",
|
||||
"Sell price": "سعر البيع",
|
||||
"Cost price": "سعر الكلفة",
|
||||
"User": "المستخدم",
|
||||
"Category": "تصنيف",
|
||||
"Note": "ملحوظة",
|
||||
"Quantity on hand": "كمية في اليد",
|
||||
"Purchase description": "وصف الشراء",
|
||||
"Sell description": "وصف البيع",
|
||||
"Sell account": "حساب البيع",
|
||||
"Cost account": "حساب التكلفة",
|
||||
"Inventory account": "حساب المخزون",
|
||||
"Payment date": "تاريخ الدفع",
|
||||
"Payment account": "حساب الدفع",
|
||||
"Amount": "كمية",
|
||||
"Reference No.": "رقم المرجع.",
|
||||
"Published": "نشرت",
|
||||
"Journal number": "رقم القيد",
|
||||
"Status": "حالة",
|
||||
"Journal type": "نوع القيد",
|
||||
"Date": "تاريخ",
|
||||
"Asset": "أصل",
|
||||
"Liability": "التزام",
|
||||
"First-in first-out (FIFO)": "الوارد أولاً يصرف أولاً (FIFO)",
|
||||
"Last-in first-out (LIFO)": "الوارد أخيرًا يصرف أولاً (LIFO)",
|
||||
"Average rate": "المعدل المتوسط",
|
||||
"Total": "الإجمالي",
|
||||
"Transaction type": "نوع المعاملة",
|
||||
"Transaction #": "عملية #",
|
||||
"Running Value": "القيمة الجارية",
|
||||
"Running quantity": "الكمية الجارية",
|
||||
"Profit Margin": "هامش الربح",
|
||||
"Value": "القيمة",
|
||||
"Rate": "السعر",
|
||||
"OPERATING ACTIVITIES": "الأنشطة التشغيلية",
|
||||
"FINANCIAL ACTIVITIES": "الأنشطة التمويلية",
|
||||
"INVESTMENT ACTIVITIES": "الانشطة الاستثمارية",
|
||||
"Net income": "صافي الدخل",
|
||||
"Adjustments net income by operating activities.": "تسويات صافي الدخل من الأنشطة التشغيلية.",
|
||||
"Net cash provided by operating activities": "صافي التدفقات النقدية من أنشطة التشغيل",
|
||||
"Net cash provided by investing activities": "صافي التدفقات النقدية من أنشطة الاستثمار",
|
||||
"Net cash provided by financing activities": "صافي التدفقات النقدية من أنشطة التمويلية",
|
||||
"Cash at beginning of period": "التدفقات النقدية في بداية الفترة",
|
||||
"NET CASH INCREASE FOR PERIOD": "زيادة التدفقات النقدية للفترة",
|
||||
"CASH AT END OF PERIOD": "صافي التدفقات النقدية في نهاية الفترة",
|
||||
"Expenses": "مصاريف",
|
||||
"Services": "خدمات",
|
||||
"Inventory": "المخزون",
|
||||
"Non Inventory": "غير المخزون",
|
||||
"Draft": "مسودة",
|
||||
"Delivered": "تم التوصيل",
|
||||
"Overdue": "متأخر",
|
||||
"Partially paid": "المدفوعة جزئيا",
|
||||
"Paid": "مدفوع",
|
||||
"Opened": "افتتح",
|
||||
"Unpaid": "غير مدفوعة",
|
||||
"Approved": "وافق",
|
||||
"Rejected": "مرفوض",
|
||||
"Invoiced": "مفوترة",
|
||||
"Expired": "منتهي الصلاحية",
|
||||
"Closed": "مغلق",
|
||||
"Manual journal": "قيد اليدوي",
|
||||
"Owner contribution": "زيادة رأس المال",
|
||||
"Transfer to account": "تحويل إلى الحساب",
|
||||
"Transfer from account": "تحويل من الحساب",
|
||||
"Other income": "إيراد اخر",
|
||||
"Other expense": "مصاريف أخرى",
|
||||
"Owner drawing": "سحب رأس المال",
|
||||
"Inventory adjustment": "تسوية المخزون",
|
||||
"Customer opening balance": "الرصيد الافتتاحي للزبون",
|
||||
"Vendor opening balance": "رصيد افتتاحي للمورد",
|
||||
"Payment made": "سند الزبون",
|
||||
"Bill": "فاتورة الشراء",
|
||||
"Payment receive": "استلام الدفع",
|
||||
"Sale receipt": "إيصال البيع",
|
||||
"Sale invoice": "فاتورة البيع",
|
||||
"Quantity": "الكمية",
|
||||
"Bank Account": "حساب البنك",
|
||||
"Saving Bank Account": "حساب التوفير البنكي",
|
||||
"Undeposited Funds": "الأموال غير المودعة",
|
||||
"Computer Equipment": "معدات كمبيوتر",
|
||||
"Office Equipment": "معدات مكتبية",
|
||||
"Uncategorized Income": "الدخل غير مصنف",
|
||||
"Sales of Service Income": "دخل مبيعات الخدمات",
|
||||
"Bank Fees and Charges": "رسوم المصرفية",
|
||||
"Exchange Gain or Loss": "ربح أو خسارة فروقات الصرف",
|
||||
"Rent": "إيجار",
|
||||
"Office expenses": "مصاريف المكتب",
|
||||
"Other Expenses": "مصاريف اخري",
|
||||
"Drawings": "السحوبات",
|
||||
"Owner's Equity": "حقوق الملكية",
|
||||
"Opening Balance Equity": "الارصدة الافتتاحية ",
|
||||
"Retained Earnings": "الأرباح المحتجزة",
|
||||
"Sales Tax Payable": "ضريبة المبيعات المستحقة",
|
||||
"Revenue Received in Advance": "الإيرادات المقبوضة مقدما",
|
||||
"Opening Balance Liabilities": "رصيد الالتزامات الافتتاحي",
|
||||
"Loan": "اقراض",
|
||||
"Owner A Drawings": "مسحوبات المالك",
|
||||
"An account that holds valuation of products or goods that availiable for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.",
|
||||
"Tracks the gain and losses of the exchange differences.": "يسجل مكاسب وخسائر فروق الصرف.",
|
||||
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "يتم تسجيل أي رسوم مصرفية يتم فرضها في حساب الرسوم والمصروفات البنكية. ومن الأمثلة على ذلك رسوم صيانة الحساب المصرفي ورسوم المعاملات ورسوم الدفع المتأخر.",
|
||||
"The income activities are not associated to the core business.": "لا ترتبط انشطة الدخل إلى الأعمال الأساسية.",
|
||||
"Cash and cash equivalents": "النقد والنقد المكافئ",
|
||||
"Inventories": "مخزون البضاعة",
|
||||
"Other current assets": "الأصول متداولة الأخرى",
|
||||
"Non-Current Assets": "أصول غير المتداولة",
|
||||
"Current Liabilties": "التزامات متداولة",
|
||||
"Long-Term Liabilities": "التزامات طويلة الاجل",
|
||||
"Non-Current Liabilities": "التزامات غير متداولة",
|
||||
"Liabilities and Equity": "التزامات وحقوق الملكية",
|
||||
"Closing balance": "الرصيد الختامي",
|
||||
"Opening balance": "الرصيد الفتاحي",
|
||||
"Total {{accountName}}": "إجمالي {{accountName}}",
|
||||
|
||||
"invoice.paper.invoice": "فاتورة",
|
||||
"invoice.paper.due_amount": "القيمة المستحقة",
|
||||
"invoice.paper.billed_to": "فاتورة إلي",
|
||||
"invoice.paper.invoice_date": "تاريخ الفاتورة",
|
||||
"invoice.paper.invoice_number": "رقم الفاتورة",
|
||||
"invoice.paper.due_date": "تاريخ الاستحقاق",
|
||||
"invoice.paper.conditions_title": "الشروط والأحكام",
|
||||
"invoice.paper.notes_title": "ملاحظات",
|
||||
"invoice.paper.total": "المجموع",
|
||||
"invoice.paper.balance_due": "مبلغ المستحق",
|
||||
"invoice.paper.payment_amount": "مبلغ المدفوع",
|
||||
"invoice.paper.invoice_amount": "قيمة الفاتورة",
|
||||
|
||||
"item_entry.paper.item_name": "اسم الصنف",
|
||||
"item_entry.paper.rate": "السعر",
|
||||
"item_entry.paper.quantity": "الكمية",
|
||||
"item_entry.paper.total": "إجمالي",
|
||||
|
||||
"estimate.paper.estimate": "عرض أسعار",
|
||||
"estimate.paper.billed_to": "عرض أسعار إلي",
|
||||
"estimate.paper.estimate_date": "تاريخ العرض",
|
||||
"estimate.paper.estimate_number": "رقم العرض",
|
||||
"estimate.paper.expiration_date": "تاريخ انتهاء الصلاحية",
|
||||
"estimate.paper.conditions_title": "الشروط والأحكام",
|
||||
"estimate.paper.notes_title": "ملاحظات",
|
||||
"estimate.paper.amount": "قيمة العرض",
|
||||
"estimate.paper.subtotal": "المجموع",
|
||||
"estimate.paper.total": "إجمالي",
|
||||
"estimate.paper.estimate_amount": "قيمة العرض",
|
||||
|
||||
"receipt.paper.receipt": "إيصال",
|
||||
"receipt.paper.billed_to": "الإيصال إلي",
|
||||
"receipt.paper.receipt_date": "تاريخ الإيصال",
|
||||
"receipt.paper.receipt_number": "رقم الإيصال",
|
||||
"receipt.paper.conditions_title": "الشروط والأحكام",
|
||||
"receipt.paper.notes_title": "ملاحظات",
|
||||
"receipt.paper.receipt_amount": "قيمة الإيصال",
|
||||
"receipt.paper.total": "إجمالي",
|
||||
"receipt.paper.payment_amount": "مبلغ المدفوع",
|
||||
"receipt.paper.balance_due": "مبلغ المستحق",
|
||||
"receipt.paper.statement": "البيان",
|
||||
"receipt.paper.notes": "ملاحظات",
|
||||
|
||||
"payment.paper.payment_receipt": "إيصال قبض",
|
||||
"payment.paper.amount_received": "القيمة المستلمه",
|
||||
"payment.paper.billed_to": "إيصال إلي",
|
||||
"payment.paper.payment_date": "تاريخ الدفع",
|
||||
"payment.paper.invoice_number": "رقم الفاتورة",
|
||||
"payment.paper.invoice_date": "تاريخ الفاتورة",
|
||||
"payment.paper.invoice_amount": "قيمة الفاتورة",
|
||||
"payment.paper.payment_amount": "قيمة الدفع",
|
||||
"payment.paper.balance_due": "المبلغ المستحق",
|
||||
"payment.paper.statement": "البيان",
|
||||
|
||||
"credit.paper.credit_note": "اشعار دائن",
|
||||
"credit.paper.amount": "قيمة الاشعار",
|
||||
"credit.paper.remaining": "رصيد المتبقي",
|
||||
"credit.paper.billed_to": "إيصال إلي",
|
||||
"credit.paper.credit_date": "تاريخ الاشعار",
|
||||
"credit.paper.terms_conditions": "الشروط والاحكام",
|
||||
"credit.paper.notes": "ملاحظات",
|
||||
"credit.paper.total": "إجمالي",
|
||||
"credit.paper.credits_used": "قيمة المستخدمه",
|
||||
"credit.paper.credits_remaining": "قيمة المتبقية",
|
||||
|
||||
"account.field.name": "إسم الحساب",
|
||||
"account.field.description": "الوصف",
|
||||
"account.field.slug": "Account slug",
|
||||
"account.field.code": "رقم الحساب",
|
||||
"account.field.root_type": "جذر الحساب",
|
||||
"account.field.normal": "طبيعة الحساب",
|
||||
"account.field.normal.credit": "دائن",
|
||||
"account.field.normal.debit": "مدين",
|
||||
"account.field.type": "نوع الحساب",
|
||||
"account.field.active": "Activity",
|
||||
"account.field.balance": "الرصيد",
|
||||
"account.field.created_at": "أنشئت في",
|
||||
"item.field.type": "نوع الصنف",
|
||||
"item.field.type.inventory": "مخزون",
|
||||
"item.field.type.service": "خدمة",
|
||||
"item.field.type.non-inventory": "غير مخزون",
|
||||
"item.field.name": "اسم الصنف",
|
||||
"item.field.code": "رمز الصنف",
|
||||
"item.field.sellable": "قابل للبيع",
|
||||
"item.field.purchasable": "قابل للشراء",
|
||||
"item.field.cost_price": "سعر التكلفة",
|
||||
"item.field.cost_account": "حساب التكلفة",
|
||||
"item.field.sell_account": "حساب البيع",
|
||||
"item.field.sell_description": "وصف البيع",
|
||||
"item.field.inventory_account": "حساب المخزون",
|
||||
"item.field.purchase_description": "وصف الشراء",
|
||||
"item.field.quantity_on_hand": "الكمية",
|
||||
"item.field.note": "ملاحظة",
|
||||
"item.field.category": "التصنيف",
|
||||
"item.field.active": "Active",
|
||||
"item.field.created_at": "أنشئت في",
|
||||
"item_category.field.name": "الاسم",
|
||||
"item_category.field.description": "الوصف",
|
||||
"item_category.field.count": "العدد",
|
||||
"item_category.field.created_at": "أنشئت في",
|
||||
"invoice.field.customer": "الزبون",
|
||||
"invoice.field.invoice_date": "تاريخ الفاتورة",
|
||||
"invoice.field.due_date": "تاريخ الاستحقاق",
|
||||
"invoice.field.invoice_no": "رقم الفاتورة",
|
||||
"invoice.field.reference_no": "رقم الإشاري",
|
||||
"invoice.field.invoice_message": "رسالة الفاتورة",
|
||||
"invoice.field.terms_conditions": "الشروط والأحكام",
|
||||
"invoice.field.amount": "القيمة",
|
||||
"invoice.field.payment_amount": "القيمة المدفوعة",
|
||||
"invoice.field.due_amount": "القيمة المستحقة",
|
||||
"invoice.field.status": "الحالة",
|
||||
"invoice.field.status.paid": "مدفوعة",
|
||||
"invoice.field.status.partially-paid": "المدفوعة جزئيا",
|
||||
"invoice.field.status.overdue": "متأخرة",
|
||||
"invoice.field.status.unpaid": "غير مدفوعة",
|
||||
"invoice.field.status.delivered": "تم تسليمها",
|
||||
"invoice.field.status.draft": "مسودة",
|
||||
"invoice.field.created_at": "أنشئت في",
|
||||
"estimate.field.amount": "القيمة",
|
||||
"estimate.field.estimate_number": "رقم العرض",
|
||||
"estimate.field.customer": "الزبون",
|
||||
"estimate.field.estimate_date": "تاريخ العرض",
|
||||
"estimate.field.expiration_date": "تاريخ انتهاء الصلاحية",
|
||||
"estimate.field.reference_no": "رقم الإشاري",
|
||||
"estimate.field.note": "ملاحظة",
|
||||
"estimate.field.terms_conditions": "الشروط والأحكام",
|
||||
"estimate.field.status": "الحالة",
|
||||
"estimate.field.status.delivered": "تم تسليمها",
|
||||
"estimate.field.status.rejected": "مرفوضة",
|
||||
"estimate.field.status.approved": "تم الموافقة",
|
||||
"estimate.field.status.draft": "مسودة",
|
||||
"estimate.field.created_at": "أنشئت في",
|
||||
"payment_receive.field.customer": "الزبون",
|
||||
"payment_receive.field.payment_date": "تاريخ الدفع",
|
||||
"payment_receive.field.amount": "القيمة",
|
||||
"payment_receive.field.reference_no": "رقم الإشاري",
|
||||
"payment_receive.field.deposit_account": "حساب الإيداع",
|
||||
"payment_receive.field.payment_receive_no": "رقم عملية الدفع",
|
||||
"payment_receive.field.statement": "البيان",
|
||||
"payment_receive.field.created_at": "أنشئت في",
|
||||
"bill_payment.field.vendor": "المورد",
|
||||
"bill_payment.field.amount": "القيمة",
|
||||
"bill_payment.field.due_amount": "قيمة المستحقة",
|
||||
"bill_payment.field.payment_account": "حساب الدفع",
|
||||
"bill_payment.field.payment_number": "قيمة الدفع",
|
||||
"bill_payment.field.payment_date": "تاريخ الدفع",
|
||||
"bill_payment.field.reference_no": "رقم الإشاري",
|
||||
"bill_payment.field.description": "الوصف",
|
||||
"bill_payment.field.created_at": "أنشئت في",
|
||||
"bill.field.vendor": "المورد",
|
||||
"bill.field.bill_number": "رقم الفاتورة",
|
||||
"bill.field.bill_date": "تاريخ الفاتورة",
|
||||
"bill.field.due_date": "تاريخ الاستحقاق",
|
||||
"bill.field.reference_no": "رقم الإشاري",
|
||||
"bill.field.status": "الحالة",
|
||||
"bill.field.status.paid": "مدفوعة",
|
||||
"bill.field.status.partially-paid": "مدفوعة جزئيا",
|
||||
"bill.field.status.unpaid": "غير مدفوعة",
|
||||
"bill.field.status.opened": "مفتوحة",
|
||||
"bill.field.status.draft": "مسودة",
|
||||
"bill.field.status.overdue": "متأخرة",
|
||||
"bill.field.amount": "القيمة",
|
||||
"bill.field.payment_amount": "قيم الدفع",
|
||||
"bill.field.note": "ملاحظة",
|
||||
"bill.field.created_at": "أنشئت في",
|
||||
"inventory_adjustment.field.date": "التاريخ",
|
||||
"inventory_adjustment.field.type": "النوع",
|
||||
"inventory_adjustment.field.type.increment": "زيادة",
|
||||
"inventory_adjustment.field.type.decrement": "نقصان",
|
||||
"inventory_adjustment.field.adjustment_account": "حساب التسوية",
|
||||
"inventory_adjustment.field.reason": "السبب",
|
||||
"inventory_adjustment.field.reference_no": "رقم الإشاري",
|
||||
"inventory_adjustment.field.description": "الوصف",
|
||||
"inventory_adjustment.field.published_at": "نشرت في",
|
||||
"inventory_adjustment.field.created_at": "أنشئت في",
|
||||
"expense.field.payment_date": "تاريخ الدفع",
|
||||
"expense.field.payment_account": "حساب الدفع",
|
||||
"expense.field.amount": "القيمة",
|
||||
"expense.field.reference_no": "رقم الإشاري",
|
||||
"expense.field.description": "الوصف",
|
||||
"expense.field.published": "Published",
|
||||
"expense.field.status": "الحالة",
|
||||
"expense.field.status.draft": "مسودة",
|
||||
"expense.field.status.published": "نشرت",
|
||||
"expense.field.created_at": "أنشئت في",
|
||||
"manual_journal.field.date": "التاريخ",
|
||||
"manual_journal.field.journal_number": "رقم القيد",
|
||||
"manual_journal.field.reference": "رقم الإشاري",
|
||||
"manual_journal.field.journal_type": "نوع القيد",
|
||||
"manual_journal.field.amount": "القيمة",
|
||||
"manual_journal.field.description": "الوصف",
|
||||
"manual_journal.field.status": "الحالة",
|
||||
"manual_journal.field.created_at": "أنشئت في",
|
||||
"receipt.field.amount": "القيمة",
|
||||
"receipt.field.deposit_account": "حساب الإيداع",
|
||||
"receipt.field.customer": "الزبون",
|
||||
"receipt.field.receipt_date": "تاريخ الإيصال",
|
||||
"receipt.field.receipt_number": "رقم الإيصال",
|
||||
"receipt.field.reference_no": "رقم الإشاري",
|
||||
"receipt.field.receipt_message": "رسالة الإيصال",
|
||||
"receipt.field.statement": "البيان",
|
||||
"receipt.field.created_at": "أنشئت في",
|
||||
"receipt.field.status": "الحالة",
|
||||
"receipt.field.status.draft": "مسودة",
|
||||
"receipt.field.status.closed": "مغلقة",
|
||||
"customer.field.first_name": "الاسم الأول",
|
||||
"customer.field.last_name": "الاسم الاخير",
|
||||
"customer.field.display_name": "اسم العرض",
|
||||
"customer.field.email": "بريد الالكتروني",
|
||||
"customer.field.work_phone": "هاتف عمل",
|
||||
"customer.field.personal_phone": "هاتف شخصي",
|
||||
"customer.field.company_name": "اسم الشركة",
|
||||
"customer.field.website": "موقع الكتروني",
|
||||
"customer.field.opening_balance_at": "الرصيد الافتتاحي في",
|
||||
"customer.field.opening_balance": "الرصيد الافتتاحي",
|
||||
"customer.field.created_at": "أنشئت في",
|
||||
"customer.field.balance": "الرصيد",
|
||||
"customer.field.status": "الحالة",
|
||||
"customer.field.currency": "العملة",
|
||||
"customer.field.status.active": "مفعل",
|
||||
"customer.field.status.inactive": "غير مفعل",
|
||||
"customer.field.status.overdue": "متأخر",
|
||||
"customer.field.status.unpaid": "غير دافع",
|
||||
"vendor.field.first_name": "الاسم الأول",
|
||||
"vendor.field.last_name": "الاسم الاخير",
|
||||
"vendor.field.display_name": "اسم العرض",
|
||||
"vendor.field.email": "بريد الالكتروني",
|
||||
"vendor.field.work_phone": "هاتف عمل",
|
||||
"vendor.field.personal_phone": "هاتف شخصي",
|
||||
"vendor.field.company_name": "اسم الشركة",
|
||||
"vendor.field.website": "موقع الكتروني",
|
||||
"vendor.field.opening_balance_at": "الرصيد الافتتاحي في",
|
||||
"vendor.field.opening_balance": "الرصيد الافتتاحي",
|
||||
"vendor.field.created_at": "أنشئت في",
|
||||
"vendor.field.balance": "الرصيد",
|
||||
"vendor.field.status": "الحالة",
|
||||
"vendor.field.currency": "العملة",
|
||||
"vendor.field.status.active": "مفعل",
|
||||
"vendor.field.status.inactive": "غير مفعل",
|
||||
"vendor.field.status.overdue": "متأخر",
|
||||
"vendor.field.status.unpaid": "غير دافع",
|
||||
"Invoice write-off": "شطب فاتورة",
|
||||
"transaction_type.credit_note": "اشعار دائن",
|
||||
"transaction_type.refund_credit_note": "استرجاع اموال اشعار دائن",
|
||||
"transaction_type.vendor_credit": "اشعار مدين",
|
||||
"transaction_type.refund_vendor_credit": "استرجاع اموال اشعار مدين",
|
||||
"transaction_type.landed_cost": "تحميل تكلفة",
|
||||
|
||||
"sms_notification.invoice_details.label": "تفاصيل فاتورة البيع ",
|
||||
"sms_notification.invoice_reminder.label": "تذكير بفاتورة البيع ",
|
||||
"sms_notification.receipt_details.label": "تفاصيل إيصال البيع ",
|
||||
"sms_notification.sale_estimate_details.label": "تفاصيل فاتورة عرض اسعار ",
|
||||
"sms_notification.payment_receive_details.label": "تفاصيل سند الزبون",
|
||||
"sms_notification.customer_balance.label": "رصيد الزبون",
|
||||
|
||||
"sms_notification.invoice_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى العميل بمجرد إنشاء الفاتورة ونشرها أو عند إشعار العميل عبر رسالة نصية قصيرة بالفاتورة. ",
|
||||
"sms_notification.payment_receive.description": "سيتم إرسال إشعار رسالة شكر للدفع إلى العميل بمجرد إنشاء الدفعة ونشرها أو إشعار العميل بالدفع يدويًا. ",
|
||||
"sms_notification.receipt_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى العميل بمجرد إنشاء ونشر الإيصال أو عند إشعار العميل بالإيصال يدويًا.",
|
||||
"sms_notification.customer_balance.description": "إرسال رسالة نصية قصيرة إشعار العملاء برصيدهم الحالي المستحق. ",
|
||||
"sms_notification.estimate_details.description": "سيتم إرسال إشعار عبر الرسائل القصيرة إلى عميلك بمجرد نشر العرض أو إشعار العميل بالعرض يدويًا.",
|
||||
"sms_notification.invoice_reminder.description": "سيتم ارسال إشعار SMS لتذكير الزبون بالدفع باكراً ، سواء ارسال بشكل تلقائي او يدوي.",
|
||||
|
||||
"sms_notification.customer_balance.default_message": "عزيزي {CustomerName} ، هذا تذكير بشأن رصيد الحالي المستحق {Balance} ، يُرجى الدفع في أقرب وقت ممكن. - {CompanyName}",
|
||||
"sms_notification.payment_receive.default_message": "مرحبًا {CustomerName} ، تم القبض بقيمة {Amount} للفاتورة - {InvoiceNumber}. نحن نتطلع إلى خدمتك مرة أخرى. شكرا لك. - {CompanyName}",
|
||||
"sms_notification.estimate.default_message": "مرحبًا , {CustomerName} ، تم أنشاء فاتورة عرض اسعار - {EstimateNumber} لك. يرجى إلقاء نظرة وقبوله للمضي قدما. بانتظار ردك. - {CompanyName}",
|
||||
|
||||
"sms_notification.invoice_details.default_message": "مرحبًا {CustomerName}, لديك مبلغ مستحق قدره {DueAmount} للفاتورة {InvoiceNumber}. - {CompanyName}",
|
||||
"sms_notification.receipt_details.default_message": "مرحبًا {CustomerName} ، لقد تم إنشاء إيصال - {ReceiptNumber} من أجلك. نتطلع إلى خدمتك مرة أخرى. شكرًا لك - {CompanyName}",
|
||||
"sms_notification.invoice_reminder.default_message": "عزيزي {CustomerName} ، يرجي سداد فاتورة - {InvoiceNumber} المستحقة. يرجى الدفع قبل تاريخ {DueDate}. شكرا لك. - {CompanyName}",
|
||||
|
||||
"module.sale_invoices.label": "فواتير البيع",
|
||||
"module.sale_receipts.label": "إيصالات البيع",
|
||||
"module.sale_estimates.label": "فاتورة عرض اسعار ",
|
||||
"module.payment_receives.label": "سندات الزبائن ",
|
||||
"module.customers.label": "العملاء",
|
||||
|
||||
"sms_notification.invoice.var.invoice_number": "يشير إلى رقم الفاتورة.",
|
||||
"sms_notification.invoice.var.reference_number": "يشير إلى رقم إشاري للفاتورة.",
|
||||
"sms_notification.invoice.var.customer_name": "يشير إلى اسم العميل الفاتورة",
|
||||
"sms_notification.invoice.var.due_amount": "يشير إلى مبلغ الفاتورة المستحق",
|
||||
"sms_notification.invoice.var.amount": "يشير إلى مبلغ الفاتورة.",
|
||||
"sms_notification.invoice.var.company_name": "يشير إلي اسم الشركة.",
|
||||
"sms_notification.invoice.var.due_date": "يشير إلي تاريخ استحقاق الفاتورة.",
|
||||
|
||||
"sms_notification.receipt.var.receipt_number": "يشير إلى رقم الإيصال.",
|
||||
"sms_notification.receipt.var.reference_number": "يشير إلى رقم الإشاري للإيصال.",
|
||||
"sms_notification.receipt.var.customer_name": "يشير إلى اسم العميل الإيصال.",
|
||||
"sms_notification.receipt.var.amount": "يشير إلى مبلغ الإيصال. ",
|
||||
"sms_notification.receipt.var.company_name": "يشير إلي اسم الشركة.",
|
||||
|
||||
"sms_notification.payment.var.payment_number": "يشير إلى رقم معاملة الدفع.",
|
||||
"sms_notification.payment.var.reference_number": "يشير إلى رقم الإشاري لعملية الدفع ",
|
||||
"sms_notification.payment.var.customer_name": "يشير إلى اسم العميل الدفع",
|
||||
"sms_notification.payment.var.amount": "يشير إلى مبلغ معاملة الدفع.",
|
||||
"sms_notification.payment.company_name": "يشير إلي اسم الشركة.",
|
||||
"sms_notification.payment.var.invoice_number": "يشير إلي رقم فاتورة التي تم دفعها.",
|
||||
|
||||
"sms_notification.estimate.var.estimate_number": "يشير إلى رقم فاتورة عرض اسعار.",
|
||||
"sms_notification.estimate.var.reference_number": "يشير إلى رقم الإشاري لفاتورة عرض اسعار.",
|
||||
"sms_notification.estimate.var.customer_name": "يشير إلى اسم العميل الفاتورة",
|
||||
"sms_notification.estimate.var.amount": "يشير إلى قيمة الفاتورة",
|
||||
"sms_notification.estimate.var.company_name": "يشير إلي اسم الشركة.",
|
||||
"sms_notification.estimate.var.expiration_date": "يشير إلي تاريخ الصلاحية الفاتورة.",
|
||||
"sms_notification.estimate.var.estimate_date": "يشير إلي تاريخ الفاتورة.",
|
||||
|
||||
"sms_notification.customer.var.customer_name": "يشير إلي اسم الزبون",
|
||||
"sms_notification.customer.var.balance": "يشير إلي رصيد زبون المستحق.",
|
||||
"sms_notification.customer.var.company_name": "يشير إلي اسم الشركة.",
|
||||
|
||||
"ability.accounts": "شجرة الحسابات",
|
||||
"ability.manual_journal": "القيود اليدوية",
|
||||
"ability.cashflow": "التدفقات النقدية",
|
||||
"ability.inventory_adjustment": "تسويات المخزون",
|
||||
"ability.customers": "الزبائن",
|
||||
"ability.vendors": "الموردين",
|
||||
"ability.sale_estimates": "فواتير عرض الاسعار",
|
||||
"ability.sale_invoices": "فواتير البيع",
|
||||
"ability.sale_receipts": "إيصالات البيع",
|
||||
"ability.expenses": "المصاريف",
|
||||
"ability.payments_receive": "سندات الزبائن",
|
||||
"ability.purchase_invoices": "فواتير الشراء",
|
||||
"ability.all_reports": "كل التقارير",
|
||||
"ability.payments_made": "سندات الموردين",
|
||||
"ability.preferences": "التفضيلات",
|
||||
"ability.mutate_system_preferences": "تعديل تفضيلات النظام.",
|
||||
|
||||
"ability.items": "الأصناف",
|
||||
"ability.view": "عرض",
|
||||
"ability.create": "إضافة",
|
||||
"ability.edit": "تعديل",
|
||||
"ability.delete": "حذف",
|
||||
"ability.transactions_locking": "إمكانية اغلاق المعاملات.",
|
||||
|
||||
"ability.balance_sheet_report": "ميزانية العمومية",
|
||||
"ability.profit_loss_sheet": "قائمة الدخل",
|
||||
"ability.journal": "اليومية العامة",
|
||||
"ability.general_ledger": "دفتر الأستاذ العام",
|
||||
"ability.cashflow_report": "تقرير التدفقات النقدية",
|
||||
"ability.AR_aging_summary_report": "ملخص اعمار الديون للذمم المدينة",
|
||||
"ability.AP_aging_summary_report": "ملخص اعمار الديون للذمم الدائنة",
|
||||
"ability.purchases_by_items": "المشتريات حسب المنتجات",
|
||||
"ability.sales_by_items_report": "المبيعات حسب المنتجات",
|
||||
"ability.customers_transactions_report": "معاملات الزبائن",
|
||||
"ability.vendors_transactions_report": "معاملات الموردين",
|
||||
"ability.customers_summary_balance_report": "ملخص أرصدة الزبائن",
|
||||
"ability.vendors_summary_balance_report": "ملخص أرصدة الموردين",
|
||||
"ability.inventory_valuation_summary": "ملخص تقييم المخزون",
|
||||
"ability.inventory_items_details": "تفاصيل منتج المخزون",
|
||||
|
||||
"vendor_credit.field.vendor": "المورد",
|
||||
"vendor_credit.field.amount": "القيمة",
|
||||
"vendor_credit.field.currency_code": "العملة",
|
||||
"vendor_credit.field.credit_date": "تاريخ الاشعار",
|
||||
"vendor_credit.field.credit_number": "رقم الاشعار",
|
||||
"vendor_credit.field.note": "ملاحظة",
|
||||
"vendor_credit.field.created_at": "أنشئت في",
|
||||
"vendor_credit.field.reference_no": "رقم الإشاري",
|
||||
|
||||
"vendor_credit.field.status": "الحالة",
|
||||
"vendor_credit.field.status.draft": "مسودة",
|
||||
"vendor_credit.field.status.published": "تم نشرها",
|
||||
"vendor_credit.field.status.open": "مفتوحة",
|
||||
"vendor_credit.field.status.closed": "مغلقة",
|
||||
|
||||
"credit_note.field.terms_conditions": "الشروط والاحكام",
|
||||
"credit_note.field.note": "ملاحظة",
|
||||
"credit_note.field.currency_code": "العملة",
|
||||
"credit_note.field.created_at": "أنشئت في",
|
||||
"credit_note.field.amount": "القيمة",
|
||||
"credit_note.field.credit_note_number": "رقم الاشعار",
|
||||
"credit_note.field.credit_note_date": "تاريخ الاشعار",
|
||||
"credit_note.field.customer": "الزبون",
|
||||
"credit_note.field.reference_no": "رقم الإشاري",
|
||||
|
||||
"credit_note.field.status": "الحالة",
|
||||
"credit_note.field.status.draft": "مسودة",
|
||||
"credit_note.field.status.published": "تم نشرها",
|
||||
"credit_note.field.status.open": "مفتوحة",
|
||||
"credit_note.field.status.closed": "مغلقة",
|
||||
|
||||
"transactions_locking.module.sales.label": "المبيعات",
|
||||
"transactions_locking.module.purchases.label": "المشتريات",
|
||||
"transactions_locking.module.financial.label": "المالية",
|
||||
"transactions_locking.module.all_transactions": "كل المعاملات",
|
||||
|
||||
"transactions_locking.module.sales.desc": "فواتير البيع ، والإيصالات ، والإشعارات الدائنة ، واستلام مدفوعات الزبائن ، والأرصدة الافتتاحية للزبائن.",
|
||||
"transactions_locking.module.purchases.desc": "فواتير الشراء ومدفوعات الموردين وإشعارات المدينة والأرصدة الافتتاحية للموردين.",
|
||||
"transactions_locking.module.financial.desc": "القيود اليدوية والمصروفات وتسويات المخزون.",
|
||||
|
||||
"inventory_adjustment.type.increment": "زيادة",
|
||||
"inventory_adjustment.type.decrement": "نقصان",
|
||||
|
||||
"customer.type.individual": "فرد",
|
||||
"customer.type.business": "اعمال",
|
||||
|
||||
"credit_note.view.draft": "مسودة",
|
||||
"credit_note.view.closed": "مغلقة",
|
||||
"credit_note.view.open": "مفتوحة",
|
||||
"credit_note.view.published": "نشرت",
|
||||
|
||||
"vendor_credit.view.draft": "مسودة",
|
||||
"vendor_credit.view.closed": "مغلقة",
|
||||
"vendor_credit.view.open": "مفتوحة",
|
||||
"vendor_credit.view.published": "نشرت",
|
||||
|
||||
"allocation_method.value.label": "القيمة",
|
||||
"allocation_method.quantity.label": "الكمية",
|
||||
|
||||
"balance_sheet.assets": "الأصول",
|
||||
"balance_sheet.current_asset": "الأصول المتداولة",
|
||||
"balance_sheet.cash_and_cash_equivalents": "النقدية وما يعادلها",
|
||||
"balance_sheet.accounts_receivable": "الذمم المدينة",
|
||||
"balance_sheet.inventory": "المخزون",
|
||||
"balance_sheet.other_current_assets": "اصول متداولة اخرى",
|
||||
"balance_sheet.fixed_asset": "الأصول الثابتة",
|
||||
"balance_sheet.non_current_assets": "الاصول غير المتداولة",
|
||||
"balance_sheet.liabilities_and_equity": "الالتزامات وحقوق الملكية",
|
||||
"balance_sheet.liabilities": "الإلتزامات",
|
||||
"balance_sheet.current_liabilties": "الالتزامات المتداولة",
|
||||
"balance_sheet.long_term_liabilities": "الالتزامات طويلة الاجل",
|
||||
"balance_sheet.non_current_liabilities": "الالتزامات غير المتداولة",
|
||||
"balance_sheet.equity": "حقوق الملكية",
|
||||
|
||||
"balance_sheet.account_name": "اسم الحساب",
|
||||
"balance_sheet.total": "إجمالي",
|
||||
"balance_sheet.percentage_of_column": "٪ التغير العمودي",
|
||||
"balance_sheet.percentage_of_row": "٪ التغير الأفقي",
|
||||
|
||||
"financial_sheet.previoud_period_date": "(ف.س) {{date}}",
|
||||
"fianncial_sheet.previous_period_change": "التغيرات (ف.س)",
|
||||
"financial_sheet.previous_period_percentage": "٪ التغير (ف.س)",
|
||||
|
||||
"financial_sheet.previous_year_date": "(س.س) {{date}}",
|
||||
"financial_sheet.previous_year_change": "التغيرات (س.س)",
|
||||
"financial_sheet.previous_year_percentage": "٪ التغير (س.س)",
|
||||
"financial_sheet.total_row": "إجمالي {{value}}",
|
||||
|
||||
"profit_loss_sheet.income": "الإيرادات",
|
||||
"profit_loss_sheet.cost_of_sales": "تكلفة المبيعات",
|
||||
"profit_loss_sheet.gross_profit": "إجمالي الدخل",
|
||||
"profit_loss_sheet.expenses": "المصروفات",
|
||||
"profit_loss_sheet.net_operating_income": "صافي الدخل التشغيلي",
|
||||
"profit_loss_sheet.other_income": "إيرادات اخري",
|
||||
"profit_loss_sheet.other_expenses": "مصاريف اخري",
|
||||
"profit_loss_sheet.net_income": "صافي الدخل",
|
||||
|
||||
"profit_loss_sheet.account_name": "اسم الحساب",
|
||||
"profit_loss_sheet.total": "إجمالي",
|
||||
|
||||
"profit_loss_sheet.percentage_of_income": "٪ التغير في الإيرادات",
|
||||
"profit_loss_sheet.percentage_of_expenses": "٪ التغير في المصاريف",
|
||||
"profit_loss_sheet.percentage_of_column": "٪ التغير العمودي",
|
||||
"profit_loss_sheet.percentage_of_row": "٪ التغير الأفقي",
|
||||
|
||||
"warehouses.primary_warehouse": "المستودع الرئيسي",
|
||||
"branches.head_branch": "الفرع الرئيسي",
|
||||
|
||||
"account.accounts_payable.currency": "الذمم الدائنة - {{currency}}",
|
||||
"account.accounts_receivable.currency": "الذمم المدينة - {{currency}}",
|
||||
|
||||
"role.admin.name": "الادارة",
|
||||
"role.admin.desc": "وصول غير مقيد لجميع الوحدات.",
|
||||
|
||||
"role.staff.name": "العاملين",
|
||||
"role.staff.desc": "الوصول إلى جميع الوحدات باستثناء التقارير والإعدادات والمحاسبة.",
|
||||
|
||||
"warehouse_transfer.view.draft.name": "مسودة",
|
||||
"warehouse_transfer.view.in_transit.name": "في النقل",
|
||||
"warehouse_transfer.view.transferred.name": "تم النقل"
|
||||
}
|
||||
641
packages/server/resources/locales/en.json
Normal file
641
packages/server/resources/locales/en.json
Normal file
@@ -0,0 +1,641 @@
|
||||
{
|
||||
"Petty Cash": "Petty Cash",
|
||||
"Cash": "Cash",
|
||||
"Bank": "Bank",
|
||||
"Other Income": "Other Income",
|
||||
"Interest Income": "Interest Income",
|
||||
"Depreciation Expense": "Depreciation Expense",
|
||||
"Interest Expense": "Interest Expense",
|
||||
"Sales of Product Income": "Sales of Product Income",
|
||||
"Inventory Asset": "Inventory Asset",
|
||||
"Cost of Goods Sold (COGS)": "Cost of Goods Sold (COGS)",
|
||||
"Cost of Goods Sold": "Cost of Goods Sold",
|
||||
"Accounts Payable": "Accounts Payable",
|
||||
"Other Expense": "Other Expense",
|
||||
"Payroll Expenses": "Payroll Expenses",
|
||||
"Fixed Asset": "Fixed Asset",
|
||||
"Credit Card": "Credit Card",
|
||||
"Non-Current Asset": "Non-Current Asset",
|
||||
"Current Asset": "Current Asset",
|
||||
"Other Asset": "Other Asset",
|
||||
"Long Term Liability": "Long Term Liability",
|
||||
"Current Liability": "Current Liability",
|
||||
"Other Liability": "Other Liability",
|
||||
"Equity": "Equity",
|
||||
"Expense": "Expense",
|
||||
"Income": "Income",
|
||||
"Accounts Receivable (A/R)": "Accounts Receivable (A/R)",
|
||||
"Accounts Receivable": "Accounts Receivable",
|
||||
"Accounts Payable (A/P)": "Accounts Payable (A/P)",
|
||||
"Inactive": "Inactive",
|
||||
"Other Current Asset": "Other Current Asset",
|
||||
"Tax Payable": "Tax Payable",
|
||||
"Other Current Liability": "Other Current Liability",
|
||||
"Non-Current Liability": "Non-Current Liability",
|
||||
"Assets": "Assets",
|
||||
"Liabilities": "Liabilities",
|
||||
"Account name": "Account name",
|
||||
"Account type": "Account type",
|
||||
"Account normal": "Account normal",
|
||||
"Description": "Description",
|
||||
"Account code": "Account code",
|
||||
"Currency": "Currency",
|
||||
"Balance": "Balance",
|
||||
"Active": "Active",
|
||||
"Created at": "Created at",
|
||||
"fixed_asset": "Fixed asset",
|
||||
"Journal": "Journal",
|
||||
"Reconciliation": "Reconciliation",
|
||||
"Credit": "Credit",
|
||||
"Debit": "Debit",
|
||||
"Interest": "Interest",
|
||||
"Depreciation": "Depreciation",
|
||||
"Payroll": "Payroll",
|
||||
"Type": "Type",
|
||||
"Name": "Name",
|
||||
"Sellable": "Sellable",
|
||||
"Purchasable": "Purchasable",
|
||||
"Sell price": "Sell price",
|
||||
"Cost price": "Cost price",
|
||||
"User": "User",
|
||||
"Category": "Category",
|
||||
"Note": "Note",
|
||||
"Quantity on hand": "Quantity on hand",
|
||||
"Quantity": "Quantity",
|
||||
"Purchase description": "Purchase description",
|
||||
"Sell description": "Sell description",
|
||||
"Sell account": "Sell account",
|
||||
"Cost account": "Cost account",
|
||||
"Inventory account": "Inventory account",
|
||||
"Payment date": "Payment date",
|
||||
"Payment account": "Payment account",
|
||||
"Amount": "Amount",
|
||||
"Reference No.": "Reference No.",
|
||||
"Journal number": "Journal number",
|
||||
"Status": "Status",
|
||||
"Journal type": "Journal type",
|
||||
"Date": "Date",
|
||||
"Asset": "Asset",
|
||||
"Liability": "Liability",
|
||||
"First-in first-out (FIFO)": "First-in first-out (FIFO)",
|
||||
"Last-in first-out (LIFO)": "Last-in first-out (LIFO)",
|
||||
"Average rate": "Average rate",
|
||||
"Total": "Total",
|
||||
"Transaction type": "Transaction type",
|
||||
"Transaction #": "Transaction #",
|
||||
"Running Value": "Running Value",
|
||||
"Running quantity": "Running quantity",
|
||||
"Profit Margin": "Profit Margin",
|
||||
"Value": "Value",
|
||||
"Rate": "Rate",
|
||||
"OPERATING ACTIVITIES": "OPERATING ACTIVITIES",
|
||||
"FINANCIAL ACTIVITIES": "FINANCIAL ACTIVITIES",
|
||||
"Net income": "Net income",
|
||||
"Adjustments net income by operating activities.": "Adjustments net income by operating activities.",
|
||||
"Net cash provided by operating activities": "Net cash provided by operating activities",
|
||||
"Net cash provided by investing activities": "Net cash provided by investing activities",
|
||||
"Net cash provided by financing activities": "Net cash provided by financing activities",
|
||||
"Cash at beginning of period": "Cash at beginning of period",
|
||||
"NET CASH INCREASE FOR PERIOD": "NET CASH INCREASE FOR PERIOD",
|
||||
"CASH AT END OF PERIOD": "CASH AT END OF PERIOD",
|
||||
"Expenses": "Expenses",
|
||||
"Services": "Services",
|
||||
"Inventory": "Inventory",
|
||||
"Non Inventory": "Non Inventory",
|
||||
"Draft": "Draft",
|
||||
"Published": "Published",
|
||||
"Delivered": "Delivered",
|
||||
"Overdue": "Overdue",
|
||||
"Partially paid": "Partially paid",
|
||||
"Paid": "Paid",
|
||||
"Opened": "Opened",
|
||||
"Unpaid": "Unpaid",
|
||||
"Approved": "Approved",
|
||||
"Rejected": "Rejected",
|
||||
"Invoiced": "Invoiced",
|
||||
"Expired": "Expired",
|
||||
"Closed": "Closed",
|
||||
"Manual journal": "Manual journal",
|
||||
"Owner contribution": "Owner contribution",
|
||||
"Transfer to account": "Transfer to account",
|
||||
"Transfer from account": "Transfer from account",
|
||||
"Other income": "Other income",
|
||||
"Other expense": "Other expense",
|
||||
"Owner drawing": "Owner drawing",
|
||||
"Inventory adjustment": "Inventory adjustment",
|
||||
"Customer opening balance": "Customer opening balance",
|
||||
"Vendor opening balance": "Vendor opening balance",
|
||||
"Payment made": "Payment made",
|
||||
"Bill": "Bill",
|
||||
"Payment receive": "Payment receive",
|
||||
"Sale receipt": "Sale receipt",
|
||||
"Sale invoice": "Sale invoice",
|
||||
"Bank Account": "Bank Account",
|
||||
"Saving Bank Account": "Saving Bank Account",
|
||||
"Undeposited Funds": "Undeposited Funds",
|
||||
"Computer Equipment": "Computer Equipment",
|
||||
"Office Equipment": "Office Equipment",
|
||||
"Uncategorized Income": "Uncategorized Income",
|
||||
"Sales of Service Income": "Sales of Service Income",
|
||||
"Bank Fees and Charges": "Bank Fees and Charges",
|
||||
"Exchange Gain or Loss": "Exchange Gain or Loss",
|
||||
"Rent": "Rent",
|
||||
"Office expenses": "Office expenses",
|
||||
"Other Expenses": "Other Expenses",
|
||||
"Drawings": "Drawings",
|
||||
"Owner's Equity": "Owner's Equity",
|
||||
"Opening Balance Equity": "Opening Balance Equity",
|
||||
"Retained Earnings": "Retained Earnings",
|
||||
"Sales Tax Payable": "Sales Tax Payable",
|
||||
"Revenue Received in Advance": "Revenue Received in Advance",
|
||||
"Opening Balance Liabilities": "Opening Balance Liabilities",
|
||||
"Loan": "Loan",
|
||||
"Owner A Drawings": "Owner A Drawings",
|
||||
"An account that holds valuation of products or goods that availiable for sale.": "An account that holds valuation of products or goods that availiable for sale.",
|
||||
"Tracks the gain and losses of the exchange differences.": "Tracks the gain and losses of the exchange differences.",
|
||||
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.",
|
||||
"The income activities are not associated to the core business.": "The income activities are not associated to the core business.",
|
||||
"Cash and cash equivalents": "Cash and cash equivalents",
|
||||
"Inventories": "Inventories",
|
||||
"Other current assets": "Other current assets",
|
||||
"Non-Current Assets": "Non-Current Assets",
|
||||
"Current Liabilties": "Current Liabilties",
|
||||
"Long-Term Liabilities": "Long-Term Liabilities",
|
||||
"Non-Current Liabilities": "Non-Current Liabilities",
|
||||
"Liabilities and Equity": "Liabilities and Equity",
|
||||
"Closing balance": "Closing balance",
|
||||
"Opening Balance": "Opening balance",
|
||||
"Total {{accountName}}": "Total {{accountName}}",
|
||||
"invoice.paper.invoice": "Invoice",
|
||||
"invoice.paper.invoice_amount": "Invoice amount",
|
||||
"invoice.paper.due_amount": "Due amount",
|
||||
"invoice.paper.billed_to": "Billed to",
|
||||
"invoice.paper.invoice_date": "Invoice date",
|
||||
"invoice.paper.invoice_number": "Invoice No.",
|
||||
"invoice.paper.due_date": "Due date",
|
||||
"invoice.paper.conditions_title": "Conditions & terms",
|
||||
"invoice.paper.notes_title": "Notes",
|
||||
"invoice.paper.total": "Total",
|
||||
"invoice.paper.payment_amount": "Payment Amount",
|
||||
"invoice.paper.balance_due": "Balance Due",
|
||||
|
||||
"item_entry.paper.item_name": "Item name",
|
||||
"item_entry.paper.rate": "Rate",
|
||||
"item_entry.paper.quantity": "Quantity",
|
||||
"item_entry.paper.total": "Total",
|
||||
|
||||
"estimate.paper.estimate": "Estimate",
|
||||
"estimate.paper.estimate_amount": "Estimate amount",
|
||||
"estimate.paper.billed_to": "Billed to",
|
||||
"estimate.paper.estimate_date": "Estimate date",
|
||||
"estimate.paper.estimate_number": "Estimate number",
|
||||
"estimate.paper.expiration_date": "Expiration date",
|
||||
"estimate.paper.conditions_title": "Conditions & terms",
|
||||
"estimate.paper.notes_title": "Notes",
|
||||
"estimate.paper.amount": "Estimate amount",
|
||||
"estimate.paper.subtotal": "Subtotal",
|
||||
"estimate.paper.total": "Total",
|
||||
|
||||
"receipt.paper.receipt": "Receipt",
|
||||
"receipt.paper.billed_to": "Billed to",
|
||||
"receipt.paper.receipt_date": "Receipt date",
|
||||
"receipt.paper.receipt_number": "Receipt number",
|
||||
"receipt.paper.expiration_date": "Expiration date",
|
||||
"receipt.paper.conditions_title": "Conditions & terms",
|
||||
"receipt.paper.notes": "Notes",
|
||||
"receipt.paper.statement": "Statement",
|
||||
"receipt.paper.receipt_amount": "Receipt amount",
|
||||
"receipt.paper.total": "Total",
|
||||
"receipt.paper.balance_due": "Balance Due",
|
||||
"receipt.paper.payment_amount": "Payment Amount",
|
||||
|
||||
"credit.paper.credit_note": "Credit Note",
|
||||
"credit.paper.remaining": "Credit remaining",
|
||||
"credit.paper.amount": "Credit amount",
|
||||
"credit.paper.billed_to": "Bill to",
|
||||
"credit.paper.credit_date": "Credit date",
|
||||
"credit.paper.total": "Total",
|
||||
"credit.paper.credits_used": "Credits used",
|
||||
"credit.paper.credits_remaining": "Credits remaining",
|
||||
"credit.paper.conditions_title": "Conditions & terms",
|
||||
"credit.paper.notes": "Notes",
|
||||
|
||||
"payment.paper.payment_receipt": "Payment Receipt",
|
||||
"payment.paper.amount_received": "Amount received",
|
||||
"payment.paper.billed_to": "Billed to",
|
||||
"payment.paper.payment_date": "Payment date",
|
||||
"payment.paper.invoice_number": "Invoice number",
|
||||
"payment.paper.invoice_date": "Invoice date",
|
||||
"payment.paper.invoice_amount": "Invoice amount",
|
||||
"payment.paper.payment_amount": "Payment amount",
|
||||
"payment.paper.balance_due": "Balance Due",
|
||||
"payment.paper.statement": "Statement",
|
||||
|
||||
"account.field.name": "Account name",
|
||||
"account.field.description": "Description",
|
||||
"account.field.slug": "Account slug",
|
||||
"account.field.code": "Account code",
|
||||
"account.field.root_type": "Root type",
|
||||
"account.field.normal": "Account normal",
|
||||
"account.field.normal.credit": "Credit",
|
||||
"account.field.normal.debit": "Debit",
|
||||
"account.field.type": "Type",
|
||||
"account.field.active": "Activity",
|
||||
"account.field.balance": "Balance",
|
||||
"account.field.created_at": "Created at",
|
||||
"item.field.type": "Item type",
|
||||
"item.field.type.inventory": "Inventory",
|
||||
"item.field.type.service": "Service",
|
||||
"item.field.type.non-inventory": "Non inventory",
|
||||
"item.field.name": "Name",
|
||||
"item.field.code": "Code",
|
||||
"item.field.sellable": "Sellable",
|
||||
"item.field.purchasable": "Purchasable",
|
||||
"item.field.cost_price": "Cost price",
|
||||
"item.field.cost_account": "Cost account",
|
||||
"item.field.sell_account": "Sell account",
|
||||
"item.field.sell_description": "Sell description",
|
||||
"item.field.inventory_account": "Inventory account",
|
||||
"item.field.purchase_description": "Purchase description",
|
||||
"item.field.quantity_on_hand": "Quantity on hand",
|
||||
"item.field.note": "Note",
|
||||
"item.field.category": "Category",
|
||||
"item.field.active": "Active",
|
||||
"item.field.created_at": "Created at",
|
||||
"item_category.field.name": "Name",
|
||||
"item_category.field.description": "Description",
|
||||
"item_category.field.count": "Count",
|
||||
"item_category.field.created_at": "Created at",
|
||||
"invoice.field.customer": "Customer",
|
||||
"invoice.field.invoice_date": "Invoice date",
|
||||
"invoice.field.due_date": "Due date",
|
||||
"invoice.field.invoice_no": "Invoice No.",
|
||||
"invoice.field.reference_no": "Reference No.",
|
||||
"invoice.field.invoice_message": "Invoice message",
|
||||
"invoice.field.terms_conditions": "Terms & conditions",
|
||||
"invoice.field.amount": "Amount",
|
||||
"invoice.field.payment_amount": "Payment amount",
|
||||
"invoice.field.due_amount": "Due amount",
|
||||
"invoice.field.status": "Status",
|
||||
"invoice.field.status.paid": "Paid",
|
||||
"invoice.field.status.partially-paid": "Partially paid",
|
||||
"invoice.field.status.overdue": "Overdue",
|
||||
"invoice.field.status.unpaid": "Unpaid",
|
||||
"invoice.field.status.delivered": "Delivered",
|
||||
"invoice.field.status.draft": "Draft",
|
||||
"invoice.field.created_at": "Created at",
|
||||
"estimate.field.amount": "Amount",
|
||||
"estimate.field.estimate_number": "Estimate number",
|
||||
"estimate.field.customer": "Customer",
|
||||
"estimate.field.estimate_date": "Estimate date",
|
||||
"estimate.field.expiration_date": "Expiration date",
|
||||
"estimate.field.reference_no": "Reference No.",
|
||||
"estimate.field.note": "Note",
|
||||
"estimate.field.terms_conditions": "Terms & conditions",
|
||||
"estimate.field.status": "Status",
|
||||
"estimate.field.status.delivered": "Delivered",
|
||||
"estimate.field.status.rejected": "Rejected",
|
||||
"estimate.field.status.approved": "Approved",
|
||||
"estimate.field.status.draft": "Draft",
|
||||
"estimate.field.created_at": "Created at",
|
||||
"payment_receive.field.customer": "Customer",
|
||||
"payment_receive.field.payment_date": "Payment date",
|
||||
"payment_receive.field.amount": "Amount",
|
||||
"payment_receive.field.reference_no": "Reference No.",
|
||||
"payment_receive.field.deposit_account": "Deposit account",
|
||||
"payment_receive.field.payment_receive_no": "Payment receive No.",
|
||||
"payment_receive.field.statement": "Statement",
|
||||
"payment_receive.field.created_at": "Created at",
|
||||
"bill_payment.field.vendor": "Vendor",
|
||||
"bill_payment.field.amount": "Amount",
|
||||
"bill_payment.field.due_amount": "Due amount",
|
||||
"bill_payment.field.payment_account": "Payment account",
|
||||
"bill_payment.field.payment_number": "Payment number",
|
||||
"bill_payment.field.payment_date": "Payment date",
|
||||
"bill_payment.field.reference_no": "Reference No.",
|
||||
"bill_payment.field.description": "Description",
|
||||
"bill_payment.field.created_at": "Created at",
|
||||
"bill.field.vendor": "Vendor",
|
||||
"bill.field.bill_number": "Bill number",
|
||||
"bill.field.bill_date": "Bill date",
|
||||
"bill.field.due_date": "Due date",
|
||||
"bill.field.reference_no": "Reference No.",
|
||||
"bill.field.status": "Status",
|
||||
"bill.field.status.paid": "Paid",
|
||||
"bill.field.status.partially-paid": "Partially paid",
|
||||
"bill.field.status.unpaid": "Unpaid",
|
||||
"bill.field.status.opened": "Opened",
|
||||
"bill.field.status.draft": "Draft",
|
||||
"bill.field.status.overdue": "overdue",
|
||||
"bill.field.amount": "Amount",
|
||||
"bill.field.payment_amount": "Payment amount",
|
||||
"bill.field.note": "Note",
|
||||
"bill.field.created_at": "Created at",
|
||||
"inventory_adjustment.field.date": "Date",
|
||||
"inventory_adjustment.field.type": "Type",
|
||||
"inventory_adjustment.field.type.increment": "Increment",
|
||||
"inventory_adjustment.field.type.decrement": "Decrement",
|
||||
"inventory_adjustment.field.adjustment_account": "Adjustment account",
|
||||
"inventory_adjustment.field.reason": "Reason",
|
||||
"inventory_adjustment.field.reference_no": "Reference No.",
|
||||
"inventory_adjustment.field.description": "Description",
|
||||
"inventory_adjustment.field.published_at": "Published at",
|
||||
"inventory_adjustment.field.created_at": "Created at",
|
||||
"expense.field.payment_date": "Payment date",
|
||||
"expense.field.payment_account": "Payment account",
|
||||
"expense.field.amount": "Amount",
|
||||
"expense.field.reference_no": "Reference No.",
|
||||
"expense.field.description": "Description",
|
||||
"expense.field.published": "Published",
|
||||
"expense.field.status": "Status",
|
||||
"expense.field.status.draft": "Draft",
|
||||
"expense.field.status.published": "Published",
|
||||
"expense.field.created_at": "Created at",
|
||||
"manual_journal.field.date": "Date",
|
||||
"manual_journal.field.journal_number": "Journal number",
|
||||
"manual_journal.field.reference": "Reference No.",
|
||||
"manual_journal.field.journal_type": "Journal type",
|
||||
"manual_journal.field.amount": "Amount",
|
||||
"manual_journal.field.description": "Description",
|
||||
"manual_journal.field.status": "Status",
|
||||
"manual_journal.field.created_at": "Created at",
|
||||
"receipt.field.amount": "Amount",
|
||||
"receipt.field.deposit_account": "Deposit account",
|
||||
"receipt.field.customer": "Customer",
|
||||
"receipt.field.receipt_date": "Receipt date",
|
||||
"receipt.field.receipt_number": "Receipt number",
|
||||
"receipt.field.reference_no": "Reference No.",
|
||||
"receipt.field.receipt_message": "Receipt message",
|
||||
"receipt.field.statement": "Statement",
|
||||
"receipt.field.created_at": "Created at",
|
||||
"receipt.field.status": "Status",
|
||||
"receipt.field.status.draft": "Draft",
|
||||
"receipt.field.status.closed": "Closed",
|
||||
"customer.field.first_name": "First name",
|
||||
"customer.field.last_name": "Last name",
|
||||
"customer.field.display_name": "Display name",
|
||||
"customer.field.email": "Email",
|
||||
"customer.field.work_phone": "Work phone",
|
||||
"customer.field.personal_phone": "Personal phone",
|
||||
"customer.field.company_name": "Company name",
|
||||
"customer.field.website": "Website",
|
||||
"customer.field.opening_balance_at": "Opening balance at",
|
||||
"customer.field.opening_balance": "Opening balance",
|
||||
"customer.field.created_at": "Created at",
|
||||
"customer.field.balance": "Balance",
|
||||
"customer.field.status": "Status",
|
||||
"customer.field.currency": "Curreny",
|
||||
"customer.field.status.active": "Active",
|
||||
"customer.field.status.inactive": "Inactive",
|
||||
"customer.field.status.overdue": "Overdue",
|
||||
"customer.field.status.unpaid": "Unpaid",
|
||||
"vendor.field.first_name": "First name",
|
||||
"vendor.field.last_name": "Last name",
|
||||
"vendor.field.display_name": "Display name",
|
||||
"vendor.field.email": "Email",
|
||||
"vendor.field.work_phone": "Work phone",
|
||||
"vendor.field.personal_phone": "Personal phone",
|
||||
"vendor.field.company_name": "Company name",
|
||||
"vendor.field.website": "Website",
|
||||
"vendor.field.opening_balance_at": "Opening balance at",
|
||||
"vendor.field.opening_balance": "Opening balance",
|
||||
"vendor.field.created_at": "Created at",
|
||||
"vendor.field.balance": "Balance",
|
||||
"vendor.field.status": "Status",
|
||||
"vendor.field.currency": "Curreny",
|
||||
"vendor.field.status.active": "Active",
|
||||
"vendor.field.status.inactive": "Inactive",
|
||||
"vendor.field.status.overdue": "Overdue",
|
||||
"vendor.field.status.unpaid": "Unpaid",
|
||||
"Invoice write-off": "Invoice write-off",
|
||||
|
||||
"transaction_type.credit_note": "Credit note",
|
||||
"transaction_type.refund_credit_note": "Refund credit note",
|
||||
"transaction_type.vendor_credit": "Vendor credit",
|
||||
"transaction_type.refund_vendor_credit": "Refund vendor credit",
|
||||
"transaction_type.landed_cost": "Landed cost",
|
||||
|
||||
"sms_notification.invoice_details.label": "Sale invoice details",
|
||||
"sms_notification.invoice_reminder.label": "Sale invoice reminder",
|
||||
"sms_notification.receipt_details.label": "Sale receipt details",
|
||||
"sms_notification.sale_estimate_details.label": "Sale estimate details",
|
||||
"sms_notification.payment_receive_details.label": "Payment receive details",
|
||||
"sms_notification.customer_balance.label": "Customer balance",
|
||||
|
||||
"sms_notification.invoice_details.description": "SMS notification will be sent to your customer once invoice created and published or when notify customer via SMS about the invoice.",
|
||||
"sms_notification.payment_receive.description": "Payment thank you message notification will be sent to customer once the payment created and published or notify customer about payment manually.",
|
||||
"sms_notification.receipt_details.description": "SMS notification will be sent to your cusotmer once receipt created and published or when notify customer about the receipt manually.",
|
||||
"sms_notification.customer_balance.description": "Send SMS to notify customers about their current outstanding balance.",
|
||||
"sms_notification.estimate_details.description": "SMS notification will be sent to your customer once estimate publish or notify customer about estimate manually.",
|
||||
"sms_notification.invoice_reminder.description": "SMS notification will be sent to remind the customer to pay earliest, either automatically or manually.",
|
||||
|
||||
"sms_notification.customer_balance.default_message": "Dear {CustomerName}, This is reminder about your current outstanding balance of {Balance}, Please pay at the earliest. - {CompanyName}",
|
||||
"sms_notification.payment_receive.default_message": "'Hi, {CustomerName}, We have received your payment for the invoice - {InvoiceNumber}. We look forward to serving you again. Thank you. - {CompanyName}'",
|
||||
"sms_notification.estimate.default_message": "Hi, {CustomerName}, We have created an estimate - {EstimateNumber} for you. Please take a look and accept it to proceed further. Looking forward to hearing from you. - {CompanyName}",
|
||||
|
||||
"sms_notification.invoice_details.default_message": "Hi, {CustomerName}, You have an outstanding amount of {DueAmount} for the invoice {InvoiceNumber}. - {CompanyName}",
|
||||
"sms_notification.receipt_details.default_message": "Hi, {CustomerName}, We have created receipt - {ReceiptNumber} for you. we look forward to serveing you again. Thank your - {CompanyName}",
|
||||
"sms_notification.invoice_reminder.default_message": "Dear {CustomerName}, The payment towards the invoice - {InvoiceNumber} is due. Please pay before {DueDate}. Thank you. - {CompanyName}",
|
||||
|
||||
"module.sale_invoices.label": "Sale invoices",
|
||||
"module.sale_receipts.label": "Sale receipts",
|
||||
"module.sale_estimates.label": "Sale estimates",
|
||||
"module.payment_receives.label": "Payment receive",
|
||||
"module.customers.label": "Customers",
|
||||
|
||||
"sms_notification.invoice.var.invoice_number": "References to invoice number.",
|
||||
"sms_notification.invoice.var.reference_number": "References to invoice reference number.",
|
||||
"sms_notification.invoice.var.customer_name": "References to invoice customer name.",
|
||||
"sms_notification.invoice.var.due_amount": "References to invoice due amount.",
|
||||
"sms_notification.invoice.var.amount": "References to invoice amount.",
|
||||
"sms_notification.invoice.var.company_name": "References to company name.",
|
||||
"sms_notification.invoice.var.due_date": "References to invoice due date.",
|
||||
|
||||
"sms_notification.receipt.var.receipt_number": "References to receipt number.",
|
||||
"sms_notification.receipt.var.reference_number": "References to receipt reference number.",
|
||||
"sms_notification.receipt.var.customer_name": "References to receipt customer name.",
|
||||
"sms_notification.receipt.var.amount": "References to receipt amount.",
|
||||
"sms_notification.receipt.var.company_name": "References to company name.",
|
||||
|
||||
"sms_notification.payment.var.payment_number": "References to payment transaction number.",
|
||||
"sms_notification.payment.var.reference_number": "References to payment reference number",
|
||||
"sms_notification.payment.var.customer_name": "References to payment customer name.",
|
||||
"sms_notification.payment.var.amount": "References to payment transaction amount.",
|
||||
"sms_notification.payment.company_name": "References to company name",
|
||||
"sms_notification.payment.var.invoice_number": "Reference to payment invoice number.",
|
||||
|
||||
"sms_notification.estimate.var.estimate_number": "References to estimate number.",
|
||||
"sms_notification.estimate.var.reference_number": "References to estimate reference number.",
|
||||
"sms_notification.estimate.var.customer_name": "References to estimate customer name.",
|
||||
"sms_notification.estimate.var.amount": "References to estimate amount.",
|
||||
"sms_notification.estimate.var.company_name": "References to company name.",
|
||||
"sms_notification.estimate.var.expiration_date": "References to estimate expirtaion date.",
|
||||
"sms_notification.estimate.var.estimate_date": "References to estimate date.",
|
||||
|
||||
"sms_notification.customer.var.customer_name": "References to customer name.",
|
||||
"sms_notification.customer.var.balance": "References to customer outstanding balance.",
|
||||
"sms_notification.customer.var.company_name": "References to company name.",
|
||||
|
||||
"ability.accounts": "Chart of accounts",
|
||||
"ability.manual_journal": "Manual journals",
|
||||
"ability.cashflow": "Cash flow",
|
||||
"ability.inventory_adjustment": "Inventory adjustments",
|
||||
"ability.customers": "Customers",
|
||||
"ability.vendors": "vendors",
|
||||
"ability.sale_estimates": "Sale estimates",
|
||||
"ability.sale_invoices": "Sale invoices",
|
||||
"ability.sale_receipts": "Sale receipts",
|
||||
"ability.expenses": "Expenses",
|
||||
"ability.payments_receive": "Payments receive",
|
||||
"ability.purchase_invoices": "Purchase invoices",
|
||||
"ability.all_reports": "All reports",
|
||||
"ability.payments_made": "Payments made",
|
||||
"ability.preferences": "Preferences",
|
||||
"ability.mutate_system_preferences": "Mutate the system preferences.",
|
||||
|
||||
"ability.items": "Items",
|
||||
"ability.view": "View",
|
||||
"ability.create": "Create",
|
||||
"ability.edit": "Edit",
|
||||
"ability.delete": "Delete",
|
||||
"ability.transactions_locking": "Ability to transactions locking.",
|
||||
|
||||
"ability.balance_sheet_report": "Balance sheet.",
|
||||
"ability.profit_loss_sheet": "Profit/loss sheet",
|
||||
"ability.journal": "Journal",
|
||||
"ability.general_ledger": "General ledger",
|
||||
"ability.cashflow_report": "Cashflow",
|
||||
"ability.AR_aging_summary_report": "A/R aging summary",
|
||||
"ability.AP_aging_summary_report": "A/P aging summary",
|
||||
"ability.purchases_by_items": "Purchases by items",
|
||||
"ability.sales_by_items_report": "Sales by items",
|
||||
"ability.customers_transactions_report": "Customers transactions",
|
||||
"ability.vendors_transactions_report": "Vendors transactions",
|
||||
"ability.customers_summary_balance_report": "Customers summary balance",
|
||||
"ability.vendors_summary_balance_report": "Vendors summary balance",
|
||||
"ability.inventory_valuation_summary": "Inventory valuation summary",
|
||||
"ability.inventory_items_details": "Inventory items details",
|
||||
|
||||
"vendor_credit.field.vendor": "Vendor name",
|
||||
"vendor_credit.field.amount": "Amount",
|
||||
"vendor_credit.field.currency_code": "Currency code",
|
||||
"vendor_credit.field.credit_date": "Credit date",
|
||||
"vendor_credit.field.credit_number": "Credit number",
|
||||
"vendor_credit.field.note": "Note",
|
||||
"vendor_credit.field.created_at": "Created at",
|
||||
"vendor_credit.field.reference_no": "Reference No.",
|
||||
|
||||
"credit_note.field.terms_conditions": "Terms and conditions",
|
||||
"credit_note.field.note": "Note",
|
||||
"credit_note.field.currency_code": "Currency code",
|
||||
"credit_note.field.created_at": "Created at",
|
||||
"credit_note.field.amount": "Amount",
|
||||
"credit_note.field.credit_note_number": "Credit note number",
|
||||
"credit_note.field.credit_note_date": "Credit date",
|
||||
"credit_note.field.customer": "Customer",
|
||||
"credit_note.field.reference_no": "Reference No.",
|
||||
|
||||
"Credit note": "Credit note",
|
||||
"Vendor credit": "Vendor credit",
|
||||
"Refund credit note": "Refund credit note",
|
||||
"Refund vendor credit": "Refund vendor credit",
|
||||
"credit_note.field.status": "Status",
|
||||
"credit_note.field.status.draft": "Draft",
|
||||
"credit_note.field.status.published": "Published",
|
||||
"credit_note.field.status.open": "Open",
|
||||
"credit_note.field.status.closed": "Closed",
|
||||
|
||||
"transactions_locking.module.sales.label": "Sales",
|
||||
"transactions_locking.module.purchases.label": "Purchases",
|
||||
"transactions_locking.module.financial.label": "Financial",
|
||||
"transactions_locking.module.all_transactions": "All transactions",
|
||||
|
||||
"transactions_locking.module.sales.desc": "Sale invoices, Receipts, credit notes, customers payment receive and customers opening balances.",
|
||||
"transactions_locking.module.purchases.desc": "Purchase invoices, vendors payments, vendor credit notes and vendors opening balances.",
|
||||
"transactions_locking.module.financial.desc": "Manual journal, expenses and inventory adjustments.",
|
||||
|
||||
"inventory_adjustment.type.increment": "Increment",
|
||||
"inventory_adjustment.type.decrement": "Decrement",
|
||||
|
||||
"customer.type.individual": "Individual",
|
||||
"customer.type.business": "Business",
|
||||
|
||||
"credit_note.view.draft": "Draft",
|
||||
"credit_note.view.closed": "Closed",
|
||||
"credit_note.view.open": "Open",
|
||||
"credit_note.view.published": "Published",
|
||||
|
||||
"vendor_credit.view.draft": "Draft",
|
||||
"vendor_credit.view.closed": "Closed",
|
||||
"vendor_credit.view.open": "Open",
|
||||
"vendor_credit.view.published": "Published",
|
||||
|
||||
"allocation_method.value.label": "Value",
|
||||
"allocation_method.quantity.label": "Quantity",
|
||||
|
||||
"balance_sheet.assets": "Assets",
|
||||
"balance_sheet.current_asset": "Current Asset",
|
||||
"balance_sheet.cash_and_cash_equivalents": "Cash and cash equivalents",
|
||||
"balance_sheet.accounts_receivable": "Accounts Receivable",
|
||||
"balance_sheet.inventory": "Inventory",
|
||||
"balance_sheet.other_current_assets": "Other current assets",
|
||||
"balance_sheet.fixed_asset": "Fixed Asset",
|
||||
"balance_sheet.non_current_assets": "Non-Current Assets",
|
||||
"balance_sheet.liabilities_and_equity": "Liabilities and Equity",
|
||||
"balance_sheet.liabilities": "Liabilities",
|
||||
"balance_sheet.current_liabilties": "Current Liabilties",
|
||||
"balance_sheet.long_term_liabilities": "Long-Term Liabilities",
|
||||
"balance_sheet.non_current_liabilities": "Non-Current Liabilities",
|
||||
"balance_sheet.equity": "Equity",
|
||||
|
||||
"balance_sheet.account_name": "Account name",
|
||||
"balance_sheet.total": "Total",
|
||||
"balance_sheet.percentage_of_column": "% of Column",
|
||||
"balance_sheet.percentage_of_row": "% of Row",
|
||||
|
||||
"financial_sheet.previoud_period_date": "{{date}} (PP)",
|
||||
"fianncial_sheet.previous_period_change": "Change (PP)",
|
||||
"financial_sheet.previous_period_percentage": "% Change (PP)",
|
||||
|
||||
"financial_sheet.previous_year_date": "{{date}} (PY)",
|
||||
"financial_sheet.previous_year_change": "Change (PY)",
|
||||
"financial_sheet.previous_year_percentage": "% Change (PY)",
|
||||
"financial_sheet.total_row": "Total {{value}}",
|
||||
|
||||
"profit_loss_sheet.income": "Income",
|
||||
"profit_loss_sheet.cost_of_sales": "Cost of sales",
|
||||
"profit_loss_sheet.gross_profit": "GROSS PROFIT",
|
||||
"profit_loss_sheet.expenses": "Expenses",
|
||||
"profit_loss_sheet.net_operating_income": "NET OPERATING INCOME",
|
||||
"profit_loss_sheet.other_income": "Other income",
|
||||
"profit_loss_sheet.other_expenses": "Other expenses",
|
||||
"profit_loss_sheet.net_income": "NET INCOME",
|
||||
|
||||
"profit_loss_sheet.account_name": "Account name",
|
||||
"profit_loss_sheet.total": "Total",
|
||||
|
||||
"profit_loss_sheet.percentage_of_income": "% of Income",
|
||||
"profit_loss_sheet.percentage_of_expenses": "% of Expenses",
|
||||
"profit_loss_sheet.percentage_of_column": "% of Column",
|
||||
"profit_loss_sheet.percentage_of_row": "% of Row",
|
||||
|
||||
"contact_summary_balance.account_name": "Account name",
|
||||
"contact_summary_balance.total": "Total",
|
||||
"contact_summary_balance.percentage_column": "% of Column",
|
||||
|
||||
"warehouses.primary_warehouse": "Primary warehouse",
|
||||
"branches.head_branch": "Head Branch",
|
||||
|
||||
"account.accounts_payable.currency": "Accounts Payable (A/P) - {{currency}}",
|
||||
"account.accounts_receivable.currency": "Accounts Receivable (A/R) - {{currency}}",
|
||||
|
||||
"role.admin.name": "Admin",
|
||||
"role.admin.desc": "Unrestricted access to all modules.",
|
||||
|
||||
"role.staff.name": "Staff",
|
||||
"role.staff.desc": "Access to all modules except reports, settings and accountant.",
|
||||
|
||||
"warehouse_transfer.view.draft.name": "Draft",
|
||||
"warehouse_transfer.view.in_transit.name": "In Transit",
|
||||
"warehouse_transfer.view.transferred.name": "Transferred"
|
||||
}
|
||||
35
packages/server/resources/scss/base.scss
Normal file
35
packages/server/resources/scss/base.scss
Normal file
@@ -0,0 +1,35 @@
|
||||
@import "./normalize.scss";
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit; // 2
|
||||
text-align: -webkit-match-parent; // 3
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
direction: ltr;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
26
packages/server/resources/scss/fonts.scss
Normal file
26
packages/server/resources/scss/fonts.scss
Normal file
File diff suppressed because one or more lines are too long
19
packages/server/resources/scss/layouts/paper-layout.scss
Normal file
19
packages/server/resources/scss/layouts/paper-layout.scss
Normal file
@@ -0,0 +1,19 @@
|
||||
@import "../base.scss";
|
||||
@import "../fonts.scss";
|
||||
|
||||
body {
|
||||
background: #f8f9fa;
|
||||
text-align: left;
|
||||
-webkit-print-color-adjust: exact;
|
||||
|
||||
html[lang^='ar'] & {
|
||||
font-family: "Segoe UI";
|
||||
}
|
||||
html[lang^='en'] & {
|
||||
font-family: "Noto Sans";
|
||||
}
|
||||
|
||||
@media print {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
193
packages/server/resources/scss/modules/credit.scss
Normal file
193
packages/server/resources/scss/modules/credit.scss
Normal file
@@ -0,0 +1,193 @@
|
||||
@import "../layouts/paper-layout.scss";
|
||||
|
||||
.credit {
|
||||
text-align: left;
|
||||
padding: 45px 40px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.organization {
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.creditNumber {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper {
|
||||
|
||||
.title {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 2px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__full-amount {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
&-item {
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #444;
|
||||
margin-bottom: 2px;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
|
||||
thead th,
|
||||
tbody tr td {
|
||||
margin-bottom: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 400;
|
||||
border-bottom: none;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
}
|
||||
|
||||
thead tr th,
|
||||
tbody tr td {
|
||||
&.item {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.rate {
|
||||
width: 18%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.quantity {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.total {
|
||||
width: 21%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-after {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__table-total {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px 10px 8px 0;
|
||||
border-top: 1px solid #d5d5d5;
|
||||
|
||||
&:last-child {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
tbody tr.payment-amount td:last-child {
|
||||
color: red
|
||||
}
|
||||
|
||||
tbody tr.blanace-due td {
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__conditions,
|
||||
&__notes {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__conditions+&__notes {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
174
packages/server/resources/scss/modules/estimate.scss
Normal file
174
packages/server/resources/scss/modules/estimate.scss
Normal file
@@ -0,0 +1,174 @@
|
||||
@import "../layouts/paper-layout.scss";
|
||||
|
||||
.estimate {
|
||||
text-align: left;
|
||||
padding: 45px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.organization {
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper {
|
||||
.title {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 2px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__estimate-amount {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
&-item {
|
||||
padding-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #444;
|
||||
margin-bottom: 2px;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
|
||||
thead th,
|
||||
tbody tr td {
|
||||
margin-bottom: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 400;
|
||||
border-bottom: none;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
}
|
||||
|
||||
thead tr th,
|
||||
tbody tr td {
|
||||
&.item {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.rate {
|
||||
width: 18%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.quantity {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.total {
|
||||
width: 21%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-after {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__table-total {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px 10px 8px 0;
|
||||
border-top: 1px solid #d5d5d5;
|
||||
|
||||
&:last-child {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
tbody tr.total td {
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__footer{
|
||||
font-size: 12px;
|
||||
}
|
||||
&__conditions,
|
||||
&__notes {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
179
packages/server/resources/scss/modules/invoice.scss
Normal file
179
packages/server/resources/scss/modules/invoice.scss
Normal file
@@ -0,0 +1,179 @@
|
||||
@import "../layouts/paper-layout.scss";
|
||||
|
||||
.invoice {
|
||||
text-align: left;
|
||||
padding: 45px 40px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.organization {
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.invoiceNo {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper {
|
||||
|
||||
.title {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 2px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
&-item {
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #444;
|
||||
margin-bottom: 2px;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
|
||||
thead th,
|
||||
tbody tr td {
|
||||
margin-bottom: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 400;
|
||||
border-bottom: none;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
tbody tr td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
}
|
||||
|
||||
thead tr th,
|
||||
tbody tr td {
|
||||
&.item {
|
||||
width: 45%;
|
||||
}
|
||||
&.rate {
|
||||
width: 18%;
|
||||
text-align: right;
|
||||
}
|
||||
&.quantity {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
&.total {
|
||||
width: 21%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-after{
|
||||
display: flex;
|
||||
}
|
||||
&__table-total {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px 10px 8px 0;
|
||||
border-top: 1px solid #d5d5d5;
|
||||
|
||||
&:last-child {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
tbody tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
tbody tr.payment-amount td:last-child {
|
||||
color: red
|
||||
}
|
||||
tbody tr.blanace-due td {
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__due-amount {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer{
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__conditions,
|
||||
&__notes {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
&__conditions + &__notes{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
178
packages/server/resources/scss/modules/payment.scss
Normal file
178
packages/server/resources/scss/modules/payment.scss
Normal file
@@ -0,0 +1,178 @@
|
||||
@import "../layouts/paper-layout.scss";
|
||||
|
||||
.payment {
|
||||
text-align: left;
|
||||
padding: 45px 40px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.organization {
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
.paymentNumber {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper {
|
||||
|
||||
.title {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 2px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
&-item {
|
||||
padding-right: 10px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #444;
|
||||
margin-bottom: 2px;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
|
||||
thead th,
|
||||
tbody tr td {
|
||||
margin-bottom: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 400;
|
||||
border-bottom: none;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
tbody tr td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
}
|
||||
|
||||
thead tr th,
|
||||
tbody tr td {
|
||||
&.item {
|
||||
width: 34%;
|
||||
}
|
||||
&.date {
|
||||
width: 22%;
|
||||
text-align: right;
|
||||
}
|
||||
&.invoiceAmount {
|
||||
width: 22%;
|
||||
text-align: right;
|
||||
}
|
||||
&.paymentAmount {
|
||||
width: 22%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-after{
|
||||
display: flex;
|
||||
}
|
||||
&__table-total {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px 10px 8px 0;
|
||||
border-top: 1px solid #d5d5d5;
|
||||
|
||||
&:last-child {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
tbody tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
tbody tr.payment-amount td:last-child {
|
||||
color: red
|
||||
}
|
||||
tbody tr.blanace-due td {
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__received-amount {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer{
|
||||
font-size: 12px;
|
||||
}
|
||||
&__conditions,
|
||||
&__notes {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
p{
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
&__conditions + &__notes{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
185
packages/server/resources/scss/modules/receipt.scss
Normal file
185
packages/server/resources/scss/modules/receipt.scss
Normal file
@@ -0,0 +1,185 @@
|
||||
@import "../layouts/paper-layout.scss";
|
||||
|
||||
.receipt {
|
||||
text-align: left;
|
||||
padding: 45px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.organization {
|
||||
|
||||
.title {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.receiptNumber {
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.paper {
|
||||
|
||||
.title {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 2px;
|
||||
font-size: 32px;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__receipt-amount {
|
||||
margin-bottom: 18px;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
}
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
font-size: 13px;
|
||||
|
||||
&-item {
|
||||
padding-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.value {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #444;
|
||||
margin-bottom: 2px;
|
||||
width: 180px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
table {
|
||||
font-size: 12px;
|
||||
color: #000;
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
|
||||
thead th,
|
||||
tbody tr td {
|
||||
margin-bottom: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
thead th {
|
||||
font-weight: 400;
|
||||
border-bottom: none;
|
||||
padding: 8px;
|
||||
color: #fff;
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
tbody tr td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #cecbcb;
|
||||
}
|
||||
|
||||
thead tr th,
|
||||
tbody tr td {
|
||||
&.item {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
&.rate {
|
||||
width: 18%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.quantity {
|
||||
width: 16%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
&.total {
|
||||
width: 21%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__table-after {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__table-total {
|
||||
margin-bottom: 20px;
|
||||
width: 50%;
|
||||
float: right;
|
||||
margin-left: auto;
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
|
||||
tbody tr td {
|
||||
padding: 8px 10px 8px 0;
|
||||
border-top: 1px solid #d5d5d5;
|
||||
|
||||
&:last-child {
|
||||
width: 140px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
tbody tr:first-child td {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
tbody tr.payment-amount td:last-child {
|
||||
color: red
|
||||
}
|
||||
|
||||
tbody tr.blanace-due td {
|
||||
border-top: 3px double #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&__footer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&__conditions,
|
||||
&__notes {
|
||||
|
||||
h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
379
packages/server/resources/scss/normalize.scss
vendored
Normal file
379
packages/server/resources/scss/normalize.scss
vendored
Normal file
@@ -0,0 +1,379 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
/* 1 */
|
||||
height: 0;
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none;
|
||||
/* 1 */
|
||||
text-decoration: underline;
|
||||
/* 2 */
|
||||
text-decoration: underline dotted;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
/* 1 */
|
||||
font-size: 1em;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
line-height: 1.15;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
display: table;
|
||||
/* 1 */
|
||||
max-width: 100%;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
white-space: normal;
|
||||
/* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
padding: 0;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
7
packages/server/resources/views/PaperTemplateLayout.pug
Normal file
7
packages/server/resources/views/PaperTemplateLayout.pug
Normal file
@@ -0,0 +1,7 @@
|
||||
html(lang=locale)
|
||||
head
|
||||
title My Site - #{title}
|
||||
block head
|
||||
body
|
||||
div.paper-template
|
||||
block content
|
||||
@@ -0,0 +1,81 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/credit-rtl.css
|
||||
else
|
||||
include ../../css/modules/credit.css
|
||||
|
||||
block content
|
||||
div.credit
|
||||
div.credit__header
|
||||
div.paper
|
||||
h1.title #{__('credit.paper.credit_note')}
|
||||
if creditNote.creditNoteNumber
|
||||
span.creditNoteNumber #{creditNote.creditNoteNumber}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
|
||||
div.credit__full-amount
|
||||
div.label #{__('credit.paper.amount')}
|
||||
div.amount #{creditNote.formattedAmount}
|
||||
|
||||
div.credit__meta
|
||||
div.credit__meta-item.credit__meta-item--amount
|
||||
span.label #{__('credit.paper.remaining')}
|
||||
span.value #{creditNote.formattedCreditsRemaining}
|
||||
|
||||
div.credit__meta-item.credit__meta-item--billed-to
|
||||
span.label #{__("credit.paper.billed_to")}
|
||||
span.value #{creditNote.customer.displayName}
|
||||
|
||||
div.credit__meta-item.credit__meta-item--credit-date
|
||||
span.label #{__("credit.paper.credit_date")}
|
||||
span.value #{creditNote.formattedCreditNoteDate}
|
||||
|
||||
div.credit__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("item_entry.paper.item_name")}
|
||||
th.rate #{__("item_entry.paper.rate")}
|
||||
th.quantity #{__("item_entry.paper.quantity")}
|
||||
th.total #{__("item_entry.paper.total")}
|
||||
tbody
|
||||
each entry in creditNote.entries
|
||||
tr
|
||||
td.item
|
||||
div.title=entry.item.name
|
||||
span.description=entry.description
|
||||
td.rate=entry.rate
|
||||
td.quantity=entry.quantity
|
||||
td.total=entry.amount
|
||||
|
||||
div.credit__table-after
|
||||
div.credit__table-total
|
||||
table
|
||||
tbody
|
||||
tr.total
|
||||
td #{__('credit.paper.total')}
|
||||
td #{creditNote.formattedAmount}
|
||||
tr.payment-amount
|
||||
td #{__('credit.paper.credits_used')}
|
||||
td #{creditNote.formattedCreditsUsed}
|
||||
tr.blanace-due
|
||||
td #{__('credit.paper.credits_remaining')}
|
||||
td #{creditNote.formattedCreditsRemaining}
|
||||
|
||||
div.credit__footer
|
||||
if creditNote.termsConditions
|
||||
div.credit__conditions
|
||||
h3 #{__("credit.paper.terms_conditions")}
|
||||
p #{creditNote.termsConditions}
|
||||
|
||||
if creditNote.note
|
||||
div.credit__notes
|
||||
h3 #{__("credit.paper.notes")}
|
||||
p #{creditNote.note}
|
||||
82
packages/server/resources/views/modules/estimate-regular.pug
Normal file
82
packages/server/resources/views/modules/estimate-regular.pug
Normal file
@@ -0,0 +1,82 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/estimate-rtl.css
|
||||
else
|
||||
include ../../css/modules/estimate.css
|
||||
|
||||
block content
|
||||
div.estimate
|
||||
div.estimate__header
|
||||
div.paper
|
||||
h1.title #{__("estimate.paper.estimate")}
|
||||
span.email #{saleEstimate.estimateNumber}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
|
||||
div.estimate__estimate-amount
|
||||
div.label #{__('estimate.paper.estimate_amount')}
|
||||
div.amount #{saleEstimate.formattedAmount}
|
||||
|
||||
div.estimate__meta
|
||||
if saleEstimate.estimateNumber
|
||||
div.estimate__meta-item.estimate__meta-item--estimate-number
|
||||
span.label #{__("estimate.paper.estimate_number")}
|
||||
span.value #{saleEstimate.estimateNumber}
|
||||
|
||||
div.estimate__meta-item.estimate__meta-item--billed-to
|
||||
span.label #{__("estimate.paper.billed_to")}
|
||||
span.value #{saleEstimate.customer.displayName}
|
||||
|
||||
div.estimate__meta-item.estimate__meta-item--estimate-date
|
||||
span.label #{__("estimate.paper.estimate_date")}
|
||||
span.value #{saleEstimate.formattedEstimateDate}
|
||||
|
||||
div.estimate__meta-item.estimate__meta-item--due-date
|
||||
span.label #{__("estimate.paper.expiration_date")}
|
||||
span.value #{saleEstimate.formattedExpirationDate}
|
||||
|
||||
div.estimate__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("item_entry.paper.item_name")}
|
||||
th.rate #{__("item_entry.paper.rate")}
|
||||
th.quantity #{__("item_entry.paper.quantity")}
|
||||
th.total #{__("item_entry.paper.total")}
|
||||
tbody
|
||||
each entry in saleEstimate.entries
|
||||
tr
|
||||
td.item
|
||||
div.title=entry.item.name
|
||||
span.description=entry.description
|
||||
td.rate=entry.rate
|
||||
td.quantity=entry.quantity
|
||||
td.total=entry.amount
|
||||
|
||||
div.estimate__table-after
|
||||
div.estimate__table-total
|
||||
table
|
||||
tbody
|
||||
tr.subtotal
|
||||
td #{__('estimate.paper.subtotal')}
|
||||
td #{saleEstimate.formattedAmount}
|
||||
tr.total
|
||||
td #{__('estimate.paper.total')}
|
||||
td #{saleEstimate.formattedAmount}
|
||||
|
||||
div.estimate__footer
|
||||
if saleEstimate.termsConditions
|
||||
div.estimate__conditions
|
||||
h3 #{__("estimate.paper.conditions_title")}
|
||||
p #{saleEstimate.termsConditions}
|
||||
|
||||
if saleEstimate.note
|
||||
div.estimate__notes
|
||||
h3 #{__("estimate.paper.notes_title")}
|
||||
p #{saleEstimate.note}
|
||||
85
packages/server/resources/views/modules/invoice-regular.pug
Normal file
85
packages/server/resources/views/modules/invoice-regular.pug
Normal file
@@ -0,0 +1,85 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/invoice-rtl.css
|
||||
else
|
||||
include ../../css/modules/invoice.css
|
||||
|
||||
block content
|
||||
div.invoice
|
||||
div.invoice__header
|
||||
div.paper
|
||||
h1.title #{__("invoice.paper.invoice")}
|
||||
if saleInvoice.invoiceNo
|
||||
span.invoiceNo #{saleInvoice.invoiceNo}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
|
||||
div.invoice__due-amount
|
||||
div.label #{__('invoice.paper.invoice_amount')}
|
||||
div.amount #{saleInvoice.formattedAmount}
|
||||
|
||||
div.invoice__meta
|
||||
div.invoice__meta-item.invoice__meta-item--amount
|
||||
span.label #{__('invoice.paper.due_amount')}
|
||||
span.value #{saleInvoice.formattedDueAmount}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--billed-to
|
||||
span.label #{__("invoice.paper.billed_to")}
|
||||
span.value #{saleInvoice.customer.displayName}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--invoice-date
|
||||
span.label #{__("invoice.paper.invoice_date")}
|
||||
span.value #{saleInvoice.formattedInvoiceDate}
|
||||
|
||||
div.invoice__meta-item.invoice__meta-item--due-date
|
||||
span.label #{__("invoice.paper.due_date")}
|
||||
span.value #{saleInvoice.formattedDueDate}
|
||||
|
||||
div.invoice__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("item_entry.paper.item_name")}
|
||||
th.rate #{__("item_entry.paper.rate")}
|
||||
th.quantity #{__("item_entry.paper.quantity")}
|
||||
th.total #{__("item_entry.paper.total")}
|
||||
tbody
|
||||
each entry in saleInvoice.entries
|
||||
tr
|
||||
td.item
|
||||
div.title=entry.item.name
|
||||
span.description=entry.description
|
||||
td.rate=entry.rate
|
||||
td.quantity=entry.quantity
|
||||
td.total=entry.amount
|
||||
|
||||
div.invoice__table-after
|
||||
div.invoice__table-total
|
||||
table
|
||||
tbody
|
||||
tr.total
|
||||
td #{__('invoice.paper.total')}
|
||||
td #{saleInvoice.formattedAmount}
|
||||
tr.payment-amount
|
||||
td #{__('invoice.paper.payment_amount')}
|
||||
td #{saleInvoice.formattedPaymentAmount}
|
||||
tr.blanace-due
|
||||
td #{__('invoice.paper.balance_due')}
|
||||
td #{saleInvoice.formattedDueAmount}
|
||||
|
||||
div.invoice__footer
|
||||
if saleInvoice.termsConditions
|
||||
div.invoice__conditions
|
||||
h3 #{__("invoice.paper.conditions_title")}
|
||||
p #{saleInvoice.termsConditions}
|
||||
|
||||
if saleInvoice.invoiceMessage
|
||||
div.invoice__notes
|
||||
h3 #{__("invoice.paper.notes_title")}
|
||||
p #{saleInvoice.invoiceMessage}
|
||||
@@ -0,0 +1,67 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/payment-rtl.css
|
||||
else
|
||||
include ../../css/modules/payment.css
|
||||
|
||||
block content
|
||||
div.payment
|
||||
div.payment__header
|
||||
div.paper
|
||||
h1.title #{__("payment.paper.payment_receipt")}
|
||||
if paymentReceive.paymentReceiveNo
|
||||
span.paymentNumber #{paymentReceive.paymentReceiveNo}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
if organizationEmail
|
||||
span.email #{organizationEmail}
|
||||
|
||||
div.payment__received-amount
|
||||
div.label #{__('payment.paper.amount_received')}
|
||||
div.amount #{paymentReceive.formattedAmount}
|
||||
|
||||
div.payment__meta
|
||||
div.payment__meta-item.payment__meta-item--billed-to
|
||||
span.label #{__("payment.paper.billed_to")}
|
||||
span.value #{paymentReceive.customer.displayName}
|
||||
|
||||
div.payment__meta-item.payment__meta-item--payment-date
|
||||
span.label #{__("payment.paper.payment_date")}
|
||||
span.value #{paymentReceive.formattedPaymentDate}
|
||||
|
||||
div.payment__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("payment.paper.invoice_number")}
|
||||
th.date #{__("payment.paper.invoice_date")}
|
||||
th.invoiceAmount #{__("payment.paper.invoice_amount")}
|
||||
th.paymentAmount #{__("payment.paper.payment_amount")}
|
||||
tbody
|
||||
each entry in paymentReceive.entries
|
||||
tr
|
||||
td.item=entry.invoice.invoiceNo
|
||||
td.date=entry.invoice.formattedInvoiceDate
|
||||
td.invoiceAmount=entry.invoice.formattedAmount
|
||||
td.paymentAmount=entry.invoice.formattedPaymentAmount
|
||||
|
||||
div.payment__table-after
|
||||
div.payment__table-total
|
||||
table
|
||||
tbody
|
||||
tr.payment-amount
|
||||
td #{__('payment.paper.payment_amount')}
|
||||
td #{paymentReceive.formattedAmount}
|
||||
tr.blanace-due
|
||||
td #{__('payment.paper.balance_due')}
|
||||
td #{paymentReceive.customer.closingBalance}
|
||||
|
||||
div.payment__footer
|
||||
if paymentReceive.statement
|
||||
div.payment__notes
|
||||
h3 #{__("payment.paper.statement")}
|
||||
p #{paymentReceive.statement}
|
||||
77
packages/server/resources/views/modules/receipt-regular.pug
Normal file
77
packages/server/resources/views/modules/receipt-regular.pug
Normal file
@@ -0,0 +1,77 @@
|
||||
extends ../PaperTemplateLayout.pug
|
||||
|
||||
block head
|
||||
style
|
||||
if (isRtl)
|
||||
include ../../css/modules/receipt-rtl.css
|
||||
else
|
||||
include ../../css/modules/receipt.css
|
||||
|
||||
block content
|
||||
div.receipt
|
||||
div.receipt__header
|
||||
div.paper
|
||||
h1.title #{__("receipt.paper.receipt")}
|
||||
span.receiptNumber #{saleReceipt.receiptNumber}
|
||||
|
||||
div.organization
|
||||
h3.title #{organizationName}
|
||||
|
||||
div.receipt__receipt-amount
|
||||
div.label #{__('receipt.paper.receipt_amount')}
|
||||
div.amount #{saleReceipt.formattedAmount}
|
||||
|
||||
div.receipt__meta
|
||||
div.receipt__meta-item.receipt__meta-item--billed-to
|
||||
span.label #{__("receipt.paper.billed_to")}
|
||||
span.value #{saleReceipt.customer.displayName}
|
||||
|
||||
div.receipt__meta-item.receipt__meta-item--invoice-date
|
||||
span.label #{__("receipt.paper.receipt_date")}
|
||||
span.value #{saleReceipt.formattedReceiptDate}
|
||||
|
||||
if saleReceipt.receiptNumber
|
||||
div.receipt__meta-item.receipt__meta-item--invoice-number
|
||||
span.label #{__("receipt.paper.receipt_number")}
|
||||
span.value #{saleReceipt.receiptNumber}
|
||||
|
||||
div.receipt__table
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.item #{__("item_entry.paper.item_name")}
|
||||
th.rate #{__("item_entry.paper.rate")}
|
||||
th.quantity #{__("item_entry.paper.quantity")}
|
||||
th.total #{__("item_entry.paper.total")}
|
||||
tbody
|
||||
each entry in saleReceipt.entries
|
||||
tr
|
||||
td.item=entry.item.name
|
||||
td.rate=entry.rate
|
||||
td.quantity=entry.quantity
|
||||
td.total=entry.amount
|
||||
|
||||
div.receipt__table-after
|
||||
div.receipt__table-total
|
||||
table
|
||||
tbody
|
||||
tr.total
|
||||
td #{__('receipt.paper.total')}
|
||||
td #{saleReceipt.formattedAmount}
|
||||
tr.payment-amount
|
||||
td #{__('receipt.paper.payment_amount')}
|
||||
td #{saleReceipt.formattedAmount}
|
||||
tr.blanace-due
|
||||
td #{__('receipt.paper.balance_due')}
|
||||
td #{'$0'}
|
||||
|
||||
div.receipt__footer
|
||||
if saleReceipt.statement
|
||||
div.receipt__conditions
|
||||
h3 #{__("receipt.paper.statement")}
|
||||
p #{saleReceipt.statement}
|
||||
|
||||
if saleReceipt.receiptMessage
|
||||
div.receipt__notes
|
||||
h3 #{__("receipt.paper.notes")}
|
||||
p #{saleReceipt.receiptMessage}
|
||||
145
packages/server/scripts/gulpConfig.js
Normal file
145
packages/server/scripts/gulpConfig.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* # Gulp Configuration.
|
||||
* ------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
const RESOURCES_PATH = '../resources/';
|
||||
module.exports = {
|
||||
banner: [
|
||||
'/**',
|
||||
' * <%= pkg.name %> - <%= pkg.description %>',
|
||||
' * @version v<%= pkg.version %>',
|
||||
' * @link <%= pkg.homepage %>',
|
||||
' * @author <%= pkg.author %>',
|
||||
' * @license <%= pkg.license %>',
|
||||
'**/',
|
||||
'',
|
||||
].join('\n'),
|
||||
|
||||
// Browser Sync
|
||||
browsersync: {
|
||||
files: ['**/*', '!**.map', '!**.css'], // Exclude map files.
|
||||
notify: false, //
|
||||
open: true, // Set it to false if you don't like the broser window opening automatically.
|
||||
port: 8080, //
|
||||
proxy: 'localhost/customatic', //
|
||||
watchOptions: {
|
||||
debounceDelay: 2000, // This introduces a small delay when watching for file change events to avoid triggering too many reloads
|
||||
},
|
||||
snippetOptions: {
|
||||
whitelist: ['/wp-admin/admin-ajax.php'],
|
||||
blacklist: ['/wp-admin/**'],
|
||||
},
|
||||
},
|
||||
|
||||
// Style Related.
|
||||
style: {
|
||||
clean: ['style.css', 'style.min.css', 'style-rtl.css', 'style-rtl.min.css'],
|
||||
build: [
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/invoice.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/estimate.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/receipt.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/credit.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/scss/modules/payment.scss`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
|
||||
// minify: true, // Allow to enable/disable minify the source.
|
||||
},
|
||||
// {
|
||||
// src: './assets/sass/editor-style.scss',
|
||||
// dest: './assets/css',
|
||||
// sourcemaps: true,
|
||||
// minify: true,
|
||||
// },
|
||||
],
|
||||
// RTL builds.
|
||||
rtl: [
|
||||
{
|
||||
src: `${RESOURCES_PATH}/css/modules/invoice.css`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/css/modules/estimate.css`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/css/modules/receipt.css`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/css/modules/credit.css`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
{
|
||||
src: `${RESOURCES_PATH}/css/modules/payment.css`,
|
||||
dest: `${RESOURCES_PATH}/css/modules`,
|
||||
},
|
||||
],
|
||||
|
||||
// Browsers you care about for auto-prefixing.
|
||||
autoprefixer: {
|
||||
browsers: [
|
||||
'Android 2.3',
|
||||
'Android >= 4',
|
||||
'Chrome >= 20',
|
||||
'Firefox >= 24',
|
||||
'Explorer >= 9',
|
||||
'iOS >= 6',
|
||||
'Opera >= 12',
|
||||
'Safari >= 6',
|
||||
],
|
||||
},
|
||||
|
||||
// SASS Configuration for all builds.
|
||||
sass: {
|
||||
errLogToConsole: true,
|
||||
// outputStyle: 'compact',
|
||||
},
|
||||
|
||||
// CSS MQ Packer configuration for all builds and style tasks.
|
||||
cssMqpacker: {},
|
||||
|
||||
// CSS nano configuration for all builds.
|
||||
cssnano: {},
|
||||
|
||||
// rtlcss configuration for all builds.
|
||||
rtlcss: {},
|
||||
},
|
||||
|
||||
// Clean specific files.
|
||||
clean: [
|
||||
'**/.DS_Store',
|
||||
'./assets/js/**/*.min.js',
|
||||
'**/*.map',
|
||||
'**/*.min.css',
|
||||
'assets/js/hypernews.js',
|
||||
],
|
||||
|
||||
// Watch related.
|
||||
watch: {
|
||||
css: ['./assets/sass/**/*'],
|
||||
js: ['assets/js/**/*.js', '!assets/js/**/*.min.js'],
|
||||
images: ['./assets/images/**/*'],
|
||||
},
|
||||
};
|
||||
50
packages/server/scripts/gulpfile.js
Normal file
50
packages/server/scripts/gulpfile.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const gulp = require('gulp');
|
||||
const sass = require('sass');
|
||||
const gulpSass = require('gulp-sass')(sass); // Gulp pluign for Sass compilation.
|
||||
const mergeStream = require('merge-stream');
|
||||
|
||||
const rename = require('gulp-rename'); // Renames files E.g. style.css -> style.min.css
|
||||
|
||||
// Style related.
|
||||
const postcss = require('gulp-postcss'); // Transforming styles with JS plugins
|
||||
const rtlcss = require('rtlcss'); // Convert LTR CSS to RTL.
|
||||
|
||||
const config = require('./gulpConfig');
|
||||
|
||||
gulp.task('styles', () => {
|
||||
const builds = config.style.build.map((build) => {
|
||||
return gulp
|
||||
.src(build.src)
|
||||
.pipe(gulpSass(config.style.sass))
|
||||
.pipe(gulp.dest(build.dest));
|
||||
});
|
||||
return mergeStream(builds);
|
||||
});
|
||||
|
||||
/**
|
||||
* Task: `styles-rtl`
|
||||
*
|
||||
* This task does the following.
|
||||
* 1. Gets the source css files.
|
||||
* 2. Covert LTR CSS to RTL.
|
||||
* 3. Suffix all CSS files to `-rtl`.
|
||||
* 4. Reloads css files via browser sync stream.
|
||||
* 5. Combine matching media queries for `.min.css` version.
|
||||
* 6. Minify all CSS files.
|
||||
* 7. Reload minified css files via browser sync stream.
|
||||
*/
|
||||
gulp.task('styles-rtl', () => {
|
||||
const builds = config.style.rtl.map((build) => {
|
||||
return gulp
|
||||
.src(build.src)
|
||||
.pipe(
|
||||
postcss([
|
||||
rtlcss(config.style.rtlcss), // Convert LTR CSS to RTL.
|
||||
]),
|
||||
)
|
||||
.pipe(rename({ suffix: '-rtl' })) // Append "-rtl" to the filename.
|
||||
.pipe(gulp.dest(build.dest));
|
||||
});
|
||||
|
||||
return mergeStream(builds);
|
||||
});
|
||||
4
packages/server/scripts/install.sh
Normal file
4
packages/server/scripts/install.sh
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
npm install
|
||||
npm run build
|
||||
npm run copy-i18n
|
||||
31
packages/server/scripts/run_test_db.sh
Normal file
31
packages/server/scripts/run_test_db.sh
Normal file
@@ -0,0 +1,31 @@
|
||||
MYSQL_USER="ratteb"
|
||||
MYSQL_DATABASE="ratteb"
|
||||
MYSQL_CONTAINER_NAME="ratteb_test"
|
||||
|
||||
MYSQL_ROOT_PASSWORD="root"
|
||||
MYSQL_PASSWORD="root"
|
||||
|
||||
echo "Start the testing MySql database..."
|
||||
|
||||
docker \
|
||||
run \
|
||||
--detach \
|
||||
--env MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} \
|
||||
--env MYSQL_USER=${MYSQL_USER} \
|
||||
--env MYSQL_PASSWORD=${MYSQL_PASSWORD} \
|
||||
--env MYSQL_DATABASE=${MYSQL_DATABASE} \
|
||||
--name ${MYSQL_CONTAINER_NAME} \
|
||||
--publish 3306:3306 \
|
||||
--tmpfs /var/lib/mysql:rw \
|
||||
mysql:5.7;
|
||||
|
||||
echo "Sleeping for 10 seconds to allow time for the DB to be provisioned:"
|
||||
for i in `seq 1 10`;
|
||||
do
|
||||
echo "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Database '${MYSQL_DATABASE}' running."
|
||||
echo " Username: ${MYSQL_USER}"
|
||||
echo " Password: ${MYSQL_PASSWORD}"
|
||||
74
packages/server/scripts/webpack.config.js
Normal file
74
packages/server/scripts/webpack.config.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const path = require('path');
|
||||
const { NormalModuleReplacementPlugin } = require('webpack');
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const outputDir = '../build';
|
||||
const outputFilename = 'index.js';
|
||||
const inputEntry = './src/server.ts';
|
||||
|
||||
const webpackOptions = {
|
||||
entry: ['regenerator-runtime/runtime', inputEntry],
|
||||
target: 'node',
|
||||
mode: isDev ? 'development' : 'production',
|
||||
watch: isDev,
|
||||
watchOptions: {
|
||||
aggregateTimeout: 200,
|
||||
poll: 1000,
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, outputDir),
|
||||
filename: outputFilename,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
extensionAlias: {
|
||||
'.ts': ['.js', '.ts'],
|
||||
'.cts': ['.cjs', '.cts'],
|
||||
'.mts': ['.mjs', '.mts'],
|
||||
},
|
||||
plugins: [
|
||||
new TsconfigPathsPlugin({
|
||||
configFile: './tsconfig.json',
|
||||
extensions: ['.ts', '.tsx', '.js'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
// Ignore knex dynamic required dialects that we don't use
|
||||
new NormalModuleReplacementPlugin(
|
||||
/m[sy]sql2?|oracle(db)?|sqlite3|pg-(native|query)/,
|
||||
'noop2'
|
||||
),
|
||||
new ProgressBarPlugin(),
|
||||
],
|
||||
externals: [nodeExternals(), 'aws-sdk', 'prettier'],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.([cm]?ts|tsx|js)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
configFile: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
],
|
||||
exclude: /(node_modules)/,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (isDev) {
|
||||
webpackOptions.plugins.push(
|
||||
new RunScriptWebpackPlugin({ name: outputFilename })
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = webpackOptions;
|
||||
52
packages/server/src/api/controllers/Account/index.ts
Normal file
52
packages/server/src/api/controllers/Account/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import AuthenticatedAccount from '@/services/AuthenticatedAccount';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
|
||||
@Service()
|
||||
export default class AccountController extends BaseController {
|
||||
@Inject()
|
||||
accountService: AuthenticatedAccount;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.get('/', this.getAccount);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private getAccount = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const account = await this.accountService.getAccount(tenantId, user);
|
||||
|
||||
return res.status(200).send({ data: account });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
42
packages/server/src/api/controllers/AccountTypes.ts
Normal file
42
packages/server/src/api/controllers/AccountTypes.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import AccountsTypesService from '@/services/Accounts/AccountsTypesServices';
|
||||
|
||||
@Service()
|
||||
export default class AccountsTypesController extends BaseController {
|
||||
@Inject()
|
||||
accountsTypesService: AccountsTypesService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/', asyncMiddleware(this.getAccountTypesList.bind(this)));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts types list.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @return {Response}
|
||||
*/
|
||||
getAccountTypesList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const accountTypes = this.accountsTypesService.getAccountsTypes(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
account_types: this.transfromToResponse(accountTypes, ['label'], req),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
500
packages/server/src/api/controllers/Accounts.ts
Normal file
500
packages/server/src/api/controllers/Accounts.ts
Normal file
@@ -0,0 +1,500 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AccountsApplication } from '@/services/Accounts/AccountsApplication';
|
||||
|
||||
@Service()
|
||||
export default class AccountsController extends BaseController {
|
||||
@Inject()
|
||||
private accountsApplication: AccountsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[query('account_id').optional().isInt().toInt()],
|
||||
this.asyncMiddleware(this.accountTransactions.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
asyncMiddleware(this.activateAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
asyncMiddleware(this.inactivateAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.EDIT, AbilitySubject.Account),
|
||||
[...this.editAccountDTOSchema, ...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(AccountAction.CREATE, AbilitySubject.Account),
|
||||
[...this.createAccountDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(AccountAction.VIEW, AbilitySubject.Account),
|
||||
[...this.accountsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getAccountsList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(AccountAction.DELETE, AbilitySubject.Account),
|
||||
[...this.accountParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteAccount.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create account DTO Schema validation.
|
||||
*/
|
||||
get createAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('currency_code').optional(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Account DTO Schema validation.
|
||||
*/
|
||||
get editAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3, max: 6 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('account_type')
|
||||
.exists()
|
||||
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('parent_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get accountParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts list validation schema.
|
||||
*/
|
||||
get accountsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get closingAccountSchema() {
|
||||
return [
|
||||
check('to_account_id').exists().isNumeric().toInt(),
|
||||
check('delete_after_closing').exists().isBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async newAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const accountDTO: IAccountDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.createAccount(
|
||||
tenantId,
|
||||
accountDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The account has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit account details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async editAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: accountId } = req.params;
|
||||
const accountDTO: IAccountDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.editAccount(
|
||||
tenantId,
|
||||
accountId,
|
||||
accountDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: account.id,
|
||||
message: 'The account has been edited successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: accountId } = req.params;
|
||||
|
||||
try {
|
||||
const account = await this.accountsApplication.getAccount(
|
||||
tenantId,
|
||||
accountId
|
||||
);
|
||||
return res
|
||||
.status(200)
|
||||
.send({ account: this.transfromToResponse(account) });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given account.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteAccount(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.deleteAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The deleted account has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate the given account.
|
||||
* @param {Response} res -
|
||||
* @param {Request} req -
|
||||
* @return {Response}
|
||||
*/
|
||||
async activateAccount(req: Request, res: Response, next: Function) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.activateAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The account has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactive the given account.
|
||||
* @param {Response} res -
|
||||
* @param {Request} req -
|
||||
* @return {Response}
|
||||
*/
|
||||
async inactivateAccount(req: Request, res: Response, next: Function) {
|
||||
const { id: accountId } = req.params;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.accountsApplication.inactivateAccount(tenantId, accountId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: accountId,
|
||||
message: 'The account has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {Response}
|
||||
*/
|
||||
public async getAccountsList(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
// Filter query.
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { accounts, filterMeta } =
|
||||
await this.accountsApplication.getAccounts(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
accounts: this.transfromToResponse(accounts, 'accountTypeLabel', req),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve accounts transactions list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async accountTransactions(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const transactionsFilter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.accountsApplication.getAccountsTransactions(
|
||||
tenantId,
|
||||
transactionsFilter
|
||||
);
|
||||
return res.status(200).send({
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_name_not_unqiue') {
|
||||
return res.boom.badRequest('The given account not unique.', {
|
||||
errors: [{ type: 'ACCOUNT.NAME.NOT.UNIQUE', code: 150 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_type_not_found') {
|
||||
return res.boom.badRequest('The given account type not found.', {
|
||||
errors: [{ type: 'ACCOUNT_TYPE_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_type_not_allowed_to_changed') {
|
||||
return res.boom.badRequest(
|
||||
'Not allowed to change account type of the account.',
|
||||
{
|
||||
errors: [{ type: 'NOT.ALLOWED.TO.CHANGE.ACCOUNT.TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'parent_account_not_found') {
|
||||
return res.boom.badRequest('The parent account not found.', {
|
||||
errors: [{ type: 'PARENT_ACCOUNT_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'parent_has_different_type') {
|
||||
return res.boom.badRequest('The parent account has different type.', {
|
||||
errors: [
|
||||
{ type: 'PARENT.ACCOUNT.HAS.DIFFERENT.ACCOUNT.TYPE', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_code_not_unique') {
|
||||
return res.boom.badRequest('The given account code is not unique.', {
|
||||
errors: [{ type: 'NOT_UNIQUE_CODE', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_has_associated_transactions') {
|
||||
return res.boom.badRequest(
|
||||
'You could not delete account has associated transactions.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS', code: 800 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'account_predefined') {
|
||||
return res.boom.badRequest('You could not delete predefined account', {
|
||||
errors: [{ type: 'ACCOUNT.PREDEFINED', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'accounts_not_found') {
|
||||
return res.boom.notFound('Some of the given accounts not found.', {
|
||||
errors: [{ type: 'SOME.ACCOUNTS.NOT_FOUND', code: 1000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'predefined_accounts') {
|
||||
return res.boom.badRequest(
|
||||
'Some of the given accounts are predefined.',
|
||||
{ errors: [{ type: 'ACCOUNTS_PREDEFINED', code: 1100 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'close_account_and_to_account_not_same_type') {
|
||||
return res.boom.badRequest(
|
||||
'The close account has different root type with to account.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'CLOSE_ACCOUNT_AND_TO_ACCOUNT_NOT_SAME_TYPE',
|
||||
code: 1200,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY') {
|
||||
return res.boom.badRequest(
|
||||
'The given account type does not support multi-currency.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT_TYPE_NOT_SUPPORTS_MULTI_CURRENCY', code: 1300 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT') {
|
||||
return res.boom.badRequest(
|
||||
'You could not add account has currency different on the parent account.',
|
||||
{
|
||||
errors: [
|
||||
{ type: 'ACCOUNT_CURRENCY_NOT_SAME_PARENT_ACCOUNT', code: 1400 },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
24
packages/server/src/api/controllers/Agendash.ts
Normal file
24
packages/server/src/api/controllers/Agendash.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Router } from 'express';
|
||||
import basicAuth from 'express-basic-auth';
|
||||
import agendash from 'agendash';
|
||||
import { Container } from 'typedi';
|
||||
import config from '@/config';
|
||||
|
||||
export default class AgendashController {
|
||||
static router() {
|
||||
const router = Router();
|
||||
const agendaInstance = Container.get('agenda');
|
||||
|
||||
router.use(
|
||||
'/dash',
|
||||
basicAuth({
|
||||
users: {
|
||||
[config.agendash.user]: config.agendash.password,
|
||||
},
|
||||
challenge: true,
|
||||
}),
|
||||
agendash(agendaInstance)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
314
packages/server/src/api/controllers/Authentication.ts
Normal file
314
packages/server/src/api/controllers/Authentication.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import { Request, Response, Router } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import countries from 'country-codes-list';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import AuthenticationService from '@/services/Authentication';
|
||||
import { ILoginDTO, ISystemUser, IRegisterDTO } from '@/interfaces';
|
||||
import { ServiceError, ServiceErrors } from '@/exceptions';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
|
||||
import config from '@/config';
|
||||
|
||||
@Service()
|
||||
export default class AuthenticationController extends BaseController {
|
||||
@Inject()
|
||||
authService: AuthenticationService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/login',
|
||||
this.loginSchema,
|
||||
this.validationResult,
|
||||
LoginThrottlerMiddleware,
|
||||
asyncMiddleware(this.login.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/register',
|
||||
this.registerSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.register.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/send_reset_password',
|
||||
this.sendResetPasswordSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendResetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
router.post(
|
||||
'/reset/:token',
|
||||
this.resetPasswordSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.resetPassword.bind(this)),
|
||||
this.handlerErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Login schema.
|
||||
*/
|
||||
get loginSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('crediential').exists().isEmail(),
|
||||
check('password').exists().isLength({ min: 5 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register schema.
|
||||
*/
|
||||
get registerSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('first_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('email')
|
||||
.exists()
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('phone_number')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.phoneNumberValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('password')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('country')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.custom(this.countryValidator)
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Country validator.
|
||||
*/
|
||||
countryValidator(value, { req }) {
|
||||
const {
|
||||
countries: { whitelist, blacklist },
|
||||
} = config.registration;
|
||||
const foundCountry = countries.findOne('countryCode', value);
|
||||
|
||||
if (!foundCountry) {
|
||||
throw new Error('The country code is invalid.');
|
||||
}
|
||||
if (
|
||||
// Focus with me! In case whitelist is not empty and the given coutry is not
|
||||
// in whitelist throw the error.
|
||||
//
|
||||
// Or in case the blacklist is not empty and the given country exists
|
||||
// in the blacklist throw the goddamn error.
|
||||
(whitelist.length > 0 && whitelist.indexOf(value) === -1) ||
|
||||
(blacklist.length > 0 && blacklist.indexOf(value) !== -1)
|
||||
) {
|
||||
throw new Error('The country code is not supported yet.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Phone number validator.
|
||||
*/
|
||||
phoneNumberValidator(value, { req }) {
|
||||
const phoneNumber = parsePhoneNumber(value, req.body.country);
|
||||
|
||||
if (!phoneNumber || !phoneNumber.isValid()) {
|
||||
throw new Error('Phone number is invalid with the given country code.');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password schema.
|
||||
*/
|
||||
get resetPasswordSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('password')
|
||||
.exists()
|
||||
.isLength({ min: 5 })
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.confirm_password) {
|
||||
throw new Error("Passwords don't match");
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password validation schema.
|
||||
*/
|
||||
get sendResetPasswordSchema(): ValidationChain[] {
|
||||
return [check('email').exists().isEmail().trim().escape()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle user login.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async login(req: Request, res: Response, next: Function): Response {
|
||||
const userDTO: ILoginDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { token, user, tenant } = await this.authService.signIn(
|
||||
userDTO.crediential,
|
||||
userDTO.password
|
||||
);
|
||||
return res.status(200).send({ token, user, tenant });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization register handler.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async register(req: Request, res: Response, next: Function) {
|
||||
const registerDTO: IRegisterDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const registeredUser: ISystemUser = await this.authService.register(
|
||||
registerDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'REGISTER.SUCCESS',
|
||||
message: 'Register organization has been success.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async sendResetPassword(req: Request, res: Response, next: Function) {
|
||||
const { email } = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.authService.sendResetPassword(email);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 'SEND_RESET_PASSWORD_SUCCESS',
|
||||
message: 'The reset password message has been sent successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset password handler
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async resetPassword(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
const { password } = req.body;
|
||||
|
||||
try {
|
||||
await this.authService.resetPassword(token, password);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'RESET_PASSWORD_SUCCESS',
|
||||
message: 'The password has been reset successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service errors.
|
||||
*/
|
||||
handlerErrors(error, req: Request, res: Response, next: Function) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (
|
||||
['INVALID_DETAILS', 'invalid_password'].indexOf(error.errorType) !== -1
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVALID_DETAILS', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_INACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_INACTIVE', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'TOKEN_INVALID' ||
|
||||
error.errorType === 'TOKEN_EXPIRED'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'TOKEN_INVALID', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'USER_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EMAIL_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EMAIL.NOT.REGISTERED', code: 500 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
if (error instanceof ServiceErrors) {
|
||||
const errorReasons = [];
|
||||
|
||||
if (error.hasType('PHONE_NUMBER_EXISTS')) {
|
||||
errorReasons.push({ type: 'PHONE_NUMBER_EXISTS', code: 100 });
|
||||
}
|
||||
if (error.hasType('EMAIL_EXISTS')) {
|
||||
errorReasons.push({ type: 'EMAIL.EXISTS', code: 200 });
|
||||
}
|
||||
if (errorReasons.length > 0) {
|
||||
return res.boom.badRequest(null, { errors: errorReasons });
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
140
packages/server/src/api/controllers/BaseController.ts
Normal file
140
packages/server/src/api/controllers/BaseController.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Response, Request, NextFunction } from 'express';
|
||||
import { matchedData, validationResult } from 'express-validator';
|
||||
import accepts from 'accepts';
|
||||
import { isArray, drop, first, camelCase, snakeCase, omit, set, get } from 'lodash';
|
||||
import { mapKeysDeep } from 'utils';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
|
||||
export default class BaseController {
|
||||
/**
|
||||
* Converts plain object keys to cameCase style.
|
||||
* @param {Object} data
|
||||
*/
|
||||
protected dataToCamelCase(data) {
|
||||
return mapKeysDeep(data, (v, k) => camelCase(k));
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the body data from validation schema.
|
||||
* @param {Request} req
|
||||
* @param options
|
||||
*/
|
||||
protected matchedBodyData(req: Request, options: any = {}) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
...omit(options, ['locations']), // override any propery except locations.
|
||||
});
|
||||
return this.dataToCamelCase(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the query data from validation schema.
|
||||
* @param {Request} req
|
||||
*/
|
||||
protected matchedQueryData(req: Request) {
|
||||
const data = matchedData(req, {
|
||||
locations: ['query'],
|
||||
});
|
||||
return this.dataToCamelCase(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate validation schema middleware.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
protected validationResult(req: Request, res: Response, next: NextFunction) {
|
||||
const validationErrors = validationResult(req);
|
||||
|
||||
if (!validationErrors.isEmpty()) {
|
||||
return res.boom.badData(null, {
|
||||
code: 'validation_error',
|
||||
...validationErrors,
|
||||
});
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets localization to response object by the given path.
|
||||
* @param {Response} response -
|
||||
* @param {string} path -
|
||||
* @param {Request} req -
|
||||
*/
|
||||
private setLocalizationByPath(
|
||||
response: any,
|
||||
path: string,
|
||||
req: Request,
|
||||
) {
|
||||
const DOT = '.';
|
||||
|
||||
if (isArray(response)) {
|
||||
response.forEach((va) => {
|
||||
const currentPath = first(path.split(DOT));
|
||||
const value = get(va, currentPath);
|
||||
|
||||
if (isArray(value)) {
|
||||
const nextPath = drop(path.split(DOT)).join(DOT);
|
||||
this.setLocalizationByPath(value, nextPath, req);
|
||||
} else {
|
||||
set(va, path, req.__(value));
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const value = get(response, path);
|
||||
set(response, path, req.__(value));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the given data to response.
|
||||
* @param {any} data
|
||||
*/
|
||||
protected transfromToResponse(
|
||||
data: any,
|
||||
translatable?: string | string[],
|
||||
req?: Request
|
||||
) {
|
||||
const response = mapKeysDeep(data, (v, k) => snakeCase(k));
|
||||
|
||||
if (translatable) {
|
||||
const translatables = Array.isArray(translatable)
|
||||
? translatable
|
||||
: [translatable];
|
||||
|
||||
translatables.forEach((path) => {
|
||||
this.setLocalizationByPath(response, path, req);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Async middleware.
|
||||
* @param {function} callback
|
||||
*/
|
||||
protected asyncMiddleware(callback) {
|
||||
return asyncMiddleware(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @returns
|
||||
*/
|
||||
protected accepts(req) {
|
||||
return accepts(req);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Request} req
|
||||
* @param {string[]} types
|
||||
* @returns {string}
|
||||
*/
|
||||
protected acceptTypes(req: Request, types: string[]) {
|
||||
return this.accepts(req).types(types);
|
||||
}
|
||||
}
|
||||
335
packages/server/src/api/controllers/Branches/index.ts
Normal file
335
packages/server/src/api/controllers/Branches/index.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { Features, ICreateBranchDTO, IEditBranchDTO } from '@/interfaces';
|
||||
import { BranchesApplication } from '@/services/Branches/BranchesApplication';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { FeatureActivationGuard } from '@/api/middleware/FeatureActivationGuard';
|
||||
|
||||
@Service()
|
||||
export class BranchesController extends BaseController {
|
||||
@Inject()
|
||||
branchesApplication: BranchesApplication;
|
||||
|
||||
/**
|
||||
* Branches routes.
|
||||
* @returns {Router}
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/activate',
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateBranches),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[
|
||||
param('id').exists().isInt().toInt(),
|
||||
check('name').exists(),
|
||||
check('code').optional({ nullable: true }),
|
||||
|
||||
check('address').optional({ nullable: true }),
|
||||
check('city').optional({ nullable: true }),
|
||||
check('country').optional({ nullable: true }),
|
||||
|
||||
check('phone_number').optional({ nullable: true }),
|
||||
check('email').optional({ nullable: true }).isEmail(),
|
||||
check('website').optional({ nullable: true }).isURL(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/mark-primary',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.markBranchAsPrimary),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[param('id').exists().isInt().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getBranch),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
FeatureActivationGuard(Features.BRANCHES),
|
||||
[],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getBranches),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public createBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const createBranchDTO: ICreateBranchDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.createBranch(
|
||||
tenantId,
|
||||
createBranchDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: branch.id,
|
||||
message: 'The branch has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public editBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
const editBranchDTO: IEditBranchDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.editBranch(
|
||||
tenantId,
|
||||
branchId,
|
||||
editBranchDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: branch.id,
|
||||
message: 'The branch has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes the given branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public deleteBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.deleteBranch(tenantId, branchId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: branchId,
|
||||
message: 'The branch has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves specific branch.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getBranch = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
const branch = await this.branchesApplication.getBranch(
|
||||
tenantId,
|
||||
branchId
|
||||
);
|
||||
return res.status(200).send({ branch });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves branches list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public getBranches = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const branches = await this.branchesApplication.getBranches(tenantId);
|
||||
|
||||
return res.status(200).send({ branches });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Activates the multi-branches feature.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public activateBranches = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.activateBranches(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Multi-branches feature has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Marks the given branch as primary.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
public markBranchAsPrimary = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: branchId } = req.params;
|
||||
|
||||
try {
|
||||
await this.branchesApplication.markBranchAsPrimary(tenantId, branchId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: branchId,
|
||||
message: 'The branch has been marked as primary.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'BRANCH_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BRANCH_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MUTLI_BRANCHES_ALREADY_ACTIVATED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'MUTLI_BRANCHES_ALREADY_ACTIVATED', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COULD_NOT_DELETE_ONLY_BRANCH') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COULD_NOT_DELETE_ONLY_BRANCH', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_CODE_NOT_UNIQUE') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BRANCH_CODE_NOT_UNIQUE', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'BRANCH_HAS_ASSOCIATED_TRANSACTIONS', code: 500 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Service, Inject, Container } from 'typedi';
|
||||
import { Router } from 'express';
|
||||
import CommandCashflowTransaction from './NewCashflowTransaction';
|
||||
import DeleteCashflowTransaction from './DeleteCashflowTransaction';
|
||||
import GetCashflowTransaction from './GetCashflowTransaction';
|
||||
import GetCashflowAccounts from './GetCashflowAccounts';
|
||||
|
||||
@Service()
|
||||
export default class CashflowController {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(Container.get(GetCashflowTransaction).router());
|
||||
router.use(Container.get(GetCashflowAccounts).router());
|
||||
router.use(Container.get(CommandCashflowTransaction).router());
|
||||
router.use(Container.get(DeleteCashflowTransaction).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class DeleteCashflowTransaction extends BaseController {
|
||||
@Inject()
|
||||
deleteCashflowService: DeleteCashflowTransactionService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.delete(
|
||||
'/transactions/:transactionId',
|
||||
CheckPolicies(CashflowAction.Delete, AbilitySubject.Cashflow),
|
||||
[param('transactionId').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.deleteCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private deleteCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const { oldCashflowTransaction } =
|
||||
await this.deleteCashflowService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: oldCashflowTransaction.id,
|
||||
message: 'The cashflow transaction has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow transaction not found.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 100 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param, query } from 'express-validator';
|
||||
import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowAccountsService: GetCashflowAccountsService;
|
||||
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/accounts',
|
||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||
[
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
],
|
||||
this.asyncMiddleware(this.getCashflowAccounts),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow accounts.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCashflowAccounts = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
// Filter query.
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const cashflowAccounts =
|
||||
await this.getCashflowAccountsService.getCashflowAccounts(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
cashflow_accounts: this.transfromToResponse(cashflowAccounts),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { param } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class GetCashflowAccounts extends BaseController {
|
||||
@Inject()
|
||||
getCashflowTransactionsService: GetCashflowTransactionsService;
|
||||
|
||||
/**
|
||||
* Controller router.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/transactions/:transactionId',
|
||||
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
|
||||
[param('transactionId').exists().isInt().toInt()],
|
||||
this.asyncMiddleware(this.getCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { transactionId } = req.params;
|
||||
|
||||
try {
|
||||
const cashflowTransaction =
|
||||
await this.getCashflowTransactionsService.getCashflowTransaction(
|
||||
tenantId,
|
||||
transactionId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
cashflow_transaction: this.transfromToResponse(cashflowTransaction),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow tranasction not found.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 200 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'The given cashflow account has invalid type.',
|
||||
{
|
||||
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT_NOT_FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '../BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, CashflowAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export default class NewCashflowTransactionController extends BaseController {
|
||||
@Inject()
|
||||
private newCashflowTranscationService: NewCashflowTransactionService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/transactions',
|
||||
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
|
||||
this.newTransactionValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newCashflowTransaction),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* New cashflow transaction validation schema.
|
||||
*/
|
||||
get newTransactionValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601().toDate(),
|
||||
check('reference_no').optional({ nullable: true }).trim().escape(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isLength({ min: 3 })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('transaction_type').exists(),
|
||||
|
||||
check('amount').exists().isFloat().toFloat(),
|
||||
check('cashflow_account_id').exists().isInt().toInt(),
|
||||
check('credit_account_id').exists().isInt().toInt(),
|
||||
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('publish').default(false).isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new cashflow transaction.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private newCashflowTransaction = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, userId } = req;
|
||||
const ownerContributionDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { cashflowTransaction } =
|
||||
await this.newCashflowTranscationService.newCashflowTransaction(
|
||||
tenantId,
|
||||
ownerContributionDTO,
|
||||
userId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: cashflowTransaction.id,
|
||||
message: 'New cashflow transaction has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle the service errors.
|
||||
* @param error
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest('Cashflow accounts ids not found.', {
|
||||
errors: [{ type: 'CASHFLOW_ACCOUNTS_IDS_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_ACCOUNTS_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest('Credit accounts ids not found.', {
|
||||
errors: [{ type: 'CREDIT_ACCOUNTS_IDS_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest('Cashflow .', {
|
||||
errors: [{ type: 'CREDIT_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'Cashflow accounts should be cash or bank type.',
|
||||
{
|
||||
errors: [{ type: 'CASHFLOW_ACCOUNTS_HAS_INVALID_TYPE', code: 300 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'CASHFLOW_TRANSACTION_NOT_FOUND') {
|
||||
return res.boom.badRequest('Cashflow transaction not found.', {
|
||||
errors: [{ type: 'CASHFLOW_TRANSACTION_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
404
packages/server/src/api/controllers/Contacts/Contacts.ts
Normal file
404
packages/server/src/api/controllers/Contacts/Contacts.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import { check, param, query, body, ValidationChain } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import ContactsService from '@/services/Contacts/ContactsService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
|
||||
@Service()
|
||||
export default class ContactsController extends BaseController {
|
||||
@Inject()
|
||||
contactsService: ContactsService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/auto-complete',
|
||||
[...this.autocompleteQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.autocompleteContacts.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getContact.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inactivateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateContact.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-complete list query validation schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given contact.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async getContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
const contact = await this.contactsService.getContact(
|
||||
tenantId,
|
||||
contactId
|
||||
);
|
||||
return res.status(200).send({
|
||||
customer: this.transfromToResponse(contact),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve auto-complete contacts list.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async autocompleteContacts(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
filterRoles: [],
|
||||
sortOrder: 'asc',
|
||||
columnSortBy: 'display_name',
|
||||
limit: 10,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const contacts = await this.contactsService.autocompleteContacts(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ contacts });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('salutation')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('first_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('last_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('company_name')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('display_name')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.normalizeEmail()
|
||||
.isEmail()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('website')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.isURL()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('work_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('personal_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('billing_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('billing_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('shipping_address_1')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_2')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_city')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_country')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_email')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isEmail()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_postcode')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_phone')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('shipping_address_state')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
|
||||
check('note')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact new DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactNewDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('opening_balance')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toInt(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
body('opening_balance_at')
|
||||
.if(body('opening_balance').exists())
|
||||
.exists()
|
||||
.isISO8601(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact edit DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get contactEditDTOSchema(): ValidationChain[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get specificContactSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given contact.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.contactsService.activateContact(tenantId, contactId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The given contact activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivate the given contact.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateContact(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.contactsService.inactivateContact(tenantId, contactId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The given contact inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACT_ALREADY_ACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_ALREADY_ACTIVE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACT_ALREADY_INACTIVE') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_ALREADY_INACTIVE', code: 800 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
351
packages/server/src/api/controllers/Contacts/Customers.ts
Normal file
351
packages/server/src/api/controllers/Contacts/Customers.ts
Normal file
@@ -0,0 +1,351 @@
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { check, query } from 'express-validator';
|
||||
import ContactsController from '@/api/controllers/Contacts/Contacts';
|
||||
import CustomersService from '@/services/Contacts/CustomersService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ICustomerNewDTO,
|
||||
ICustomerEditDTO,
|
||||
AbilitySubject,
|
||||
CustomerAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { CustomersApplication } from '@/services/Contacts/Customers/CustomersApplication';
|
||||
|
||||
@Service()
|
||||
export default class CustomersController extends ContactsController {
|
||||
@Inject()
|
||||
private customersApplication: CustomersApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(CustomerAction.Create, AbilitySubject.Customer),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactNewDTOSchema,
|
||||
...this.customerDTOSchema,
|
||||
...this.createCustomerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/opening_balance',
|
||||
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
|
||||
[
|
||||
...this.specificContactSchema,
|
||||
check('opening_balance').exists().isNumeric().toFloat(),
|
||||
check('opening_balance_at').optional().isISO8601(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editOpeningBalanceCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.Edit, AbilitySubject.Customer),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactEditDTOSchema,
|
||||
...this.customerDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.Delete, AbilitySubject.Customer),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCustomersList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(CustomerAction.View, AbilitySubject.Customer),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCustomer.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer DTO schema.
|
||||
*/
|
||||
get customerDTOSchema() {
|
||||
return [
|
||||
check('customer_type')
|
||||
.exists()
|
||||
.isIn(['business', 'individual'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create customer DTO schema.
|
||||
*/
|
||||
get createCustomerDTOSchema() {
|
||||
return [
|
||||
check('currency_code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: 3 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* List param query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new customer.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: ICustomerNewDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const contact = await this.customersApplication.createCustomer(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contact.id,
|
||||
message: 'The customer has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given customer details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: ICustomerEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.customersApplication.editCustomer(
|
||||
tenantId,
|
||||
contactId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The customer has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the opening balance of the given customer.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async editOpeningBalanceCustomer(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: customerId } = req.params;
|
||||
const openingBalanceEditDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.customersApplication.editOpeningBalance(
|
||||
tenantId,
|
||||
customerId,
|
||||
openingBalanceEditDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: customerId,
|
||||
message:
|
||||
'The opening balance of the given customer has been changed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given customer id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
const customer = await this.customersApplication.getCustomer(
|
||||
tenantId,
|
||||
contactId,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
customer: this.transfromToResponse(customer),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given customer from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteCustomer(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.customersApplication.deleteCustomer(tenantId, contactId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The customer has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve customers paginated and filterable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getCustomersList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
inactiveMode: false,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { customers, pagination, filterMeta } =
|
||||
await this.customersApplication.getCustomers(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
customers: this.transfromToResponse(customers),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMERS.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CUSTOMER_HAS_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CUSTOMER_HAS_TRANSACTIONS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
332
packages/server/src/api/controllers/Contacts/Vendors.ts
Normal file
332
packages/server/src/api/controllers/Contacts/Vendors.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { body, query, ValidationChain, check } from 'express-validator';
|
||||
|
||||
import ContactsController from '@/api/controllers/Contacts/Contacts';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IVendorNewDTO,
|
||||
IVendorEditDTO,
|
||||
IVendorsFilter,
|
||||
AbilitySubject,
|
||||
VendorAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { VendorsApplication } from '@/services/Contacts/Vendors/VendorsApplication';
|
||||
|
||||
@Service()
|
||||
export default class VendorsController extends ContactsController {
|
||||
@Inject()
|
||||
private vendorsApplication: VendorsApplication;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(VendorAction.Create, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactNewDTOSchema,
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/opening_balance',
|
||||
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.specificContactSchema,
|
||||
check('opening_balance').exists().isNumeric().toFloat(),
|
||||
check('opening_balance_at').optional().isISO8601(),
|
||||
check('opening_balance_exchange_rate')
|
||||
.default(1)
|
||||
.isFloat({ gt: 0 })
|
||||
.toFloat(),
|
||||
check('opening_balance_branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editOpeningBalanceVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.Edit, AbilitySubject.Vendor),
|
||||
[
|
||||
...this.contactDTOSchema,
|
||||
...this.contactEditDTOSchema,
|
||||
...this.vendorDTOSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.Delete, AbilitySubject.Vendor),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
|
||||
[...this.specificContactSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getVendor.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(VendorAction.View, AbilitySubject.Vendor),
|
||||
[...this.vendorsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getVendorsList.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendor DTO schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get vendorDTOSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 3, max: 3 }),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vendors datatable list validation schema.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get vendorsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional().isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new vendor.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: IVendorNewDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const vendor = await this.vendorsApplication.createVendor(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: vendor.id,
|
||||
message: 'The vendor has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given vendor details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const contactDTO: IVendorEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.editVendor(
|
||||
tenantId,
|
||||
contactId,
|
||||
contactDTO,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The vendor has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the opening balance of the given vendor.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async editOpeningBalanceVendor(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: vendorId } = req.params;
|
||||
const editOpeningBalanceDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.editOpeningBalance(
|
||||
tenantId,
|
||||
vendorId,
|
||||
editOpeningBalanceDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: vendorId,
|
||||
message:
|
||||
'The opening balance of the given vendor has been changed successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given vendor from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: contactId } = req.params;
|
||||
|
||||
try {
|
||||
await this.vendorsApplication.deleteVendor(tenantId, contactId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contactId,
|
||||
message: 'The vendor has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given vendor id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getVendor(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: vendorId } = req.params;
|
||||
|
||||
try {
|
||||
const vendor = await this.vendorsApplication.getVendor(
|
||||
tenantId,
|
||||
vendorId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send(this.transfromToResponse({ vendor }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getVendorsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const vendorsFilter: IVendorsFilter = {
|
||||
inactiveMode: false,
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { vendors, pagination, filterMeta } =
|
||||
await this.vendorsApplication.getVendors(tenantId, vendorsFilter);
|
||||
|
||||
return res.status(200).send({
|
||||
vendors: this.transfromToResponse(vendors),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error -
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDORS.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'OPENING_BALANCE_DATE_REQUIRED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'OPENING_BALANCE_DATE_REQUIRED', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'VENDOR_HAS_TRANSACTIONS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'VENDOR_HAS_TRANSACTIONS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
211
packages/server/src/api/controllers/Currencies.ts
Normal file
211
packages/server/src/api/controllers/Currencies.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import CurrenciesService from '@/services/Currencies/CurrenciesService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export default class CurrenciesController extends BaseController {
|
||||
@Inject()
|
||||
currenciesService: CurrenciesService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[...this.listSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.all.bind(this))
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.currencyDTOSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.currencyIdParamSchema, ...this.currencyEditDTOSchemaValidation],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:currency_code',
|
||||
[...this.currencyParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteCurrency.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get currencyDTOSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_name').exists().trim(),
|
||||
check('currency_code').exists().trim(),
|
||||
check('currency_sign').exists().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get currencyEditDTOSchemaValidation(): ValidationChain[] {
|
||||
return [
|
||||
check('currency_name').exists().trim(),
|
||||
check('currency_sign').exists().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
get currencyIdParamSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get currencyParamSchema(): ValidationChain[] {
|
||||
return [param('currency_code').exists().trim().escape()];
|
||||
}
|
||||
|
||||
get listSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all registered currency details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async all(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const currencies = await this.currenciesService.listCurrencies(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
currencies: this.transfromToResponse(currencies),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new currency on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const currencyDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.currenciesService.newCurrency(tenantId, currencyDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
currency_code: currencyDTO.currencyCode,
|
||||
message: 'The currency has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details of the given currency.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { currency_code: currencyCode } = req.params;
|
||||
|
||||
try {
|
||||
await this.currenciesService.deleteCurrency(tenantId, currencyCode);
|
||||
return res.status(200).send({
|
||||
currency_code: currencyCode,
|
||||
message: 'The currency has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the currency.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editCurrency(req: Request, res: Response, next: Function) {
|
||||
const { tenantId } = req;
|
||||
const { id: currencyId } = req.params;
|
||||
const editCurrencyDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const currency = await this.currenciesService.editCurrency(
|
||||
tenantId,
|
||||
currencyId,
|
||||
editCurrencyDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
currency_code: currency.currencyCode,
|
||||
message: 'The currency has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles currencies service error.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'currency_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CURRENCY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'currency_code_exists') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{
|
||||
type: 'CURRENCY_CODE_EXISTS',
|
||||
message: 'The given currency code is already exists.',
|
||||
code: 200,
|
||||
}],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'CANNOT_DELETE_BASE_CURRENCY',
|
||||
code: 300,
|
||||
message: 'Cannot delete the base currency.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
47
packages/server/src/api/controllers/Dashboard/index.ts
Normal file
47
packages/server/src/api/controllers/Dashboard/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import DashboardService from '@/services/Dashboard/DashboardService';
|
||||
|
||||
@Service()
|
||||
export default class DashboardMetaController {
|
||||
@Inject()
|
||||
dashboardService: DashboardService;
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/boot', this.getDashboardBoot);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the dashboard boot meta.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
getDashboardBoot = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const authorizedUser = req.user;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const meta = await this.dashboardService.getBootMeta(
|
||||
tenantId,
|
||||
authorizedUser
|
||||
);
|
||||
|
||||
return res.status(200).send({ meta });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
220
packages/server/src/api/controllers/ExchangeRates.ts
Normal file
220
packages/server/src/api/controllers/ExchangeRates.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
|
||||
@Service()
|
||||
export default class ExchangeRatesController extends BaseController {
|
||||
@Inject()
|
||||
exchangeRatesService: ExchangeRatesService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
[...this.exchangeRatesListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.exchangeRates.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handleServiceError,
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.exchangeRateDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.addExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.exchangeRateIdSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteExchangeRate.bind(this)),
|
||||
this.handleServiceError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get exchangeRatesListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateDTOSchema() {
|
||||
return [
|
||||
check('exchange_rate').exists().isNumeric().toFloat(),
|
||||
check('currency_code').exists().trim().escape(),
|
||||
check('date').exists().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
get exchangeRateEditDTOSchema() {
|
||||
return [check('exchange_rate').exists().isNumeric().toFloat()];
|
||||
}
|
||||
|
||||
get exchangeRateIdSchema() {
|
||||
return [param('id').isNumeric().toInt()];
|
||||
}
|
||||
|
||||
get exchangeRatesIdsSchema() {
|
||||
return [
|
||||
query('ids').isArray({ min: 2 }),
|
||||
query('ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve exchange rates.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async exchangeRates(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
filterRoles: [],
|
||||
columnSortBy: 'created_at',
|
||||
sortOrder: 'asc',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
if (filter.stringifiedFilterRoles) {
|
||||
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
|
||||
}
|
||||
try {
|
||||
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({ exchange_rates: exchangeRates });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new exchange rate on the given date.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.newExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateDTO
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRate.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the given exchange rate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
const exchangeRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId,
|
||||
exchangeRateDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: exchangeRateId,
|
||||
message: 'The exchange rate has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given exchange rate from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: exchangeRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.exchangeRatesService.deleteExchangeRate(
|
||||
tenantId,
|
||||
exchangeRateId
|
||||
);
|
||||
return res.status(200).send({ id: exchangeRateId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handleServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
|
||||
return res.status(404).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
456
packages/server/src/api/controllers/Expenses/Expenses.ts
Normal file
456
packages/server/src/api/controllers/Expenses/Expenses.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ExpenseAction,
|
||||
IExpenseCreateDTO,
|
||||
IExpenseEditDTO,
|
||||
} from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ExpensesApplication } from '@/services/Expenses/ExpensesApplication';
|
||||
|
||||
@Service()
|
||||
export class ExpensesController extends BaseController {
|
||||
@Inject()
|
||||
private expensesApplication: ExpensesApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(ExpenseAction.Create, AbilitySubject.Expense),
|
||||
[...this.expenseDTOSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
|
||||
[...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.Edit, AbilitySubject.Expense),
|
||||
[...this.editExpenseDTOSchema, ...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.Delete, AbilitySubject.Expense),
|
||||
[...this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
|
||||
[...this.expensesListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getExpensesList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ExpenseAction.View, AbilitySubject.Expense),
|
||||
[this.expenseParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getExpense.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense DTO schema.
|
||||
*/
|
||||
get expenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('currency_code').optional().isString().isLength({ max: 3 }),
|
||||
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.expense_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit expense validation schema.
|
||||
*/
|
||||
get editExpenseDTOSchema() {
|
||||
return [
|
||||
check('reference_no')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('payment_date').exists().isISO8601().toDate(),
|
||||
check('payment_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('currency_code').optional().isString().isLength({ max: 3 }),
|
||||
check('exchange_rate').optional({ nullable: true }).isNumeric().toFloat(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('payee_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('categories').exists().isArray({ min: 1 }),
|
||||
check('categories.*.id').optional().isNumeric().toInt(),
|
||||
check('categories.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.expense_account_id')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('categories.*.amount')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ max: DATATYPES_LENGTH.DECIMAL_13_3 }) // 13, 3
|
||||
.toFloat(),
|
||||
check('categories.*.description')
|
||||
.optional()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
|
||||
check('categories.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expense param validation schema.
|
||||
*/
|
||||
get expenseParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Expenses list validation schema.
|
||||
*/
|
||||
get expensesListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('column_sort_by').optional(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new expense on
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async newExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const expenseDTO: IExpenseCreateDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const expense = await this.expensesApplication.createExpense(
|
||||
tenantId,
|
||||
expenseDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: expense.id,
|
||||
message: 'The expense has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits details of the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async editExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: expenseId } = req.params;
|
||||
const expenseDTO: IExpenseEditDTO = this.matchedBodyData(req);
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.editExpense(
|
||||
tenantId,
|
||||
expenseId,
|
||||
expenseDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.deleteExpense(tenantId, expenseId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishs the given expense.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async publishExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
await this.expensesApplication.publishExpense(tenantId, expenseId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
id: expenseId,
|
||||
message: 'The expense has been published successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expneses list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpensesList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { expenses, pagination, filterMeta } =
|
||||
await this.expensesApplication.getExpenses(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
expenses: this.transfromToResponse(expenses),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expense details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getExpense(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: expenseId } = req.params;
|
||||
|
||||
try {
|
||||
const expense = await this.expensesApplication.getExpense(
|
||||
tenantId,
|
||||
expenseId
|
||||
);
|
||||
return res.status(200).send(this.transfromToResponse({ expense }));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform service errors to api response errors.
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'expense_not_found') {
|
||||
return res.boom.badRequest('Expense not found.', {
|
||||
errors: [{ type: 'EXPENSE_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXPENSES_NOT_FOUND') {
|
||||
return res.boom.badRequest('Expenses not found.', {
|
||||
errors: [{ type: 'EXPENSES_NOT_FOUND', code: 110 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'total_amount_equals_zero') {
|
||||
return res.boom.badRequest('Expense total should not equal zero.', {
|
||||
errors: [{ type: 'TOTAL.AMOUNT.EQUALS.ZERO', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'payment_account_not_found') {
|
||||
return res.boom.badRequest('Payment account not found.', {
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.NOT.FOUND', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'some_expenses_not_found') {
|
||||
return res.boom.badRequest('Some expense accounts not found.', {
|
||||
errors: [{ type: 'SOME.EXPENSE.ACCOUNTS.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'payment_account_has_invalid_type') {
|
||||
return res.boom.badRequest('Payment account has invalid type.', {
|
||||
errors: [{ type: 'PAYMENT.ACCOUNT.HAS.INVALID.TYPE', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'expenses_account_has_invalid_type') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'EXPENSES.ACCOUNT.HAS.INVALID.TYPE', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'expense_already_published') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'EXPENSE_ALREADY_PUBLISHED', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contact_not_found') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CONTACT_NOT_FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EXPENSE_HAS_ASSOCIATED_LANDED_COST') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'ENTRIES_ALLOCATED_COST_COULD_NOT_DELETED', code: 1000 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES'
|
||||
) {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'LOCATED_COST_ENTRIES_SHOULD_BIGGE_THAN_NEW_ENTRIES',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
15
packages/server/src/api/controllers/Expenses/index.ts
Normal file
15
packages/server/src/api/controllers/Expenses/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import { ExpensesController } from './Expenses';
|
||||
|
||||
@Service()
|
||||
export default class ExpensesBaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ExpensesController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
107
packages/server/src/api/controllers/FinancialStatements.ts
Normal file
107
packages/server/src/api/controllers/FinancialStatements.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Router } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
|
||||
import BalanceSheetController from './FinancialStatements/BalanceSheet';
|
||||
import TrialBalanceSheetController from './FinancialStatements/TrialBalanceSheet';
|
||||
import GeneralLedgerController from './FinancialStatements/GeneralLedger';
|
||||
import JournalSheetController from './FinancialStatements/JournalSheet';
|
||||
import ProfitLossController from './FinancialStatements/ProfitLossSheet';
|
||||
import ARAgingSummary from './FinancialStatements/ARAgingSummary';
|
||||
import APAgingSummary from './FinancialStatements/APAgingSummary';
|
||||
import PurchasesByItemsController from './FinancialStatements/PurchasesByItem';
|
||||
import SalesByItemsController from './FinancialStatements/SalesByItems';
|
||||
import InventoryValuationController from './FinancialStatements/InventoryValuationSheet';
|
||||
import CustomerBalanceSummaryController from './FinancialStatements/CustomerBalanceSummary';
|
||||
import VendorBalanceSummaryController from './FinancialStatements/VendorBalanceSummary';
|
||||
import TransactionsByCustomers from './FinancialStatements/TransactionsByCustomers';
|
||||
import TransactionsByVendors from './FinancialStatements/TransactionsByVendors';
|
||||
import CashFlowStatementController from './FinancialStatements/CashFlow/CashFlow';
|
||||
import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
|
||||
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
|
||||
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
'/balance_sheet',
|
||||
Container.get(BalanceSheetController).router()
|
||||
);
|
||||
router.use(
|
||||
'/profit_loss_sheet',
|
||||
Container.get(ProfitLossController).router()
|
||||
);
|
||||
router.use(
|
||||
'/general_ledger',
|
||||
Container.get(GeneralLedgerController).router()
|
||||
);
|
||||
router.use(
|
||||
'/trial_balance_sheet',
|
||||
Container.get(TrialBalanceSheetController).router()
|
||||
);
|
||||
router.use('/journal', Container.get(JournalSheetController).router());
|
||||
router.use(
|
||||
'/receivable_aging_summary',
|
||||
Container.get(ARAgingSummary).router()
|
||||
);
|
||||
router.use(
|
||||
'/payable_aging_summary',
|
||||
Container.get(APAgingSummary).router()
|
||||
);
|
||||
router.use(
|
||||
'/purchases-by-items',
|
||||
Container.get(PurchasesByItemsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/sales-by-items',
|
||||
Container.get(SalesByItemsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/inventory-valuation',
|
||||
Container.get(InventoryValuationController).router()
|
||||
);
|
||||
router.use(
|
||||
'/customer-balance-summary',
|
||||
Container.get(CustomerBalanceSummaryController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/vendor-balance-summary',
|
||||
Container.get(VendorBalanceSummaryController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-customers',
|
||||
Container.get(TransactionsByCustomers).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-reference',
|
||||
Container.get(TransactionsByReferenceController).router(),
|
||||
);
|
||||
router.use(
|
||||
'/cashflow-account-transactions',
|
||||
Container.get(CashflowAccountTransactions).router(),
|
||||
);
|
||||
router.use(
|
||||
'/project-profitability-summary',
|
||||
Container.get(ProjectProfitabilityController).router(),
|
||||
)
|
||||
return router;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import APAgingSummaryReportService from '@/services/FinancialStatements/AgingSummary/APAgingSummaryService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
export default class APAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
APAgingSummaryService: APAgingSummaryReportService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.payableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').isInt({ min: 1 }).toInt(),
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
*/
|
||||
async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import ARAgingSummaryService from '@/services/FinancialStatements/AgingSummary/ARAgingSummaryService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
ARAgingSummaryService: ARAgingSummaryService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.receivableAgingSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* AR aging summary validation roles.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').isInt({ min: 1 }).toInt(),
|
||||
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report.
|
||||
*/
|
||||
async receivableAgingSummary(req: Request, res: Response) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BalanceSheetStatementService from '@/services/FinancialStatements/BalanceSheet/BalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import BalanceSheetTable from '@/services/FinancialStatements/BalanceSheet/BalanceSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class BalanceSheetStatementController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
balanceSheetService: BalanceSheetStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report),
|
||||
this.balanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.balanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get balanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('accounting_method').optional().isIn(['cash', 'accural']),
|
||||
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Percentage of column/row.
|
||||
query('percentage_of_column').optional().isBoolean().toBoolean(),
|
||||
query('percentage_of_row').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Camparsion periods periods.
|
||||
query('previous_period').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
// Camparsion periods periods.
|
||||
query('previous_year').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the balance sheet.
|
||||
*/
|
||||
async balanceSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.balanceSheetService.balanceSheet(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
const table = new BalanceSheetTable(data, query, i18n);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({ data, columns, query, meta });
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from "../BaseController";
|
||||
|
||||
export default class BaseFinancialReportController extends BaseController {
|
||||
|
||||
|
||||
get sheetNumberFormatValidationSchema() {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.show_zero').optional().isBoolean().toBoolean(),
|
||||
query('number_format.format_money')
|
||||
.optional()
|
||||
.isIn(['total', 'always', 'none'])
|
||||
.trim(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CashFlowStatementService from '@/services/FinancialStatements/CashFlow/CashFlowService';
|
||||
import {
|
||||
ICashFlowStatementDOO,
|
||||
ICashFlowStatement,
|
||||
AbilitySubject,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
cashFlowService: CashFlowStatementService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_CASHFLOW, AbilitySubject.Report),
|
||||
this.cashflowValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cashFlow.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get cashflowValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['date_periods', 'total']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(cashFlowDOO: ICashFlowStatementDOO) {
|
||||
const { data, query, meta } = cashFlowDOO;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
cashFlowDOO: ICashFlowStatementDOO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: cashFlowTable.tableRows(),
|
||||
columns: cashFlowTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(cashFlowDOO.query),
|
||||
meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async cashFlow(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(cashFlow, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformJsonResponse(cashFlow));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ICashFlowStatementDOO,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CashflowAccountTransactionsService from '@/services/FinancialStatements/CashflowAccountTransactions/CashflowAccountTransactionsService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class CashFlowAccountTransactionsController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
cashflowAccountTransactions: CashflowAccountTransactionsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CASHFLOW_ACCOUNT_TRANSACTION,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.cashFlow),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cashflow account transactions validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('account_id').exists().isInt().toInt(),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow account transactions statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(casahflowAccountTransactions) {
|
||||
const { transactions, pagination } = casahflowAccountTransactions;
|
||||
|
||||
return {
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
cashFlowDOO: ICashFlowStatementDOO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: cashFlowTable.tableRows(),
|
||||
columns: cashFlowTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(cashFlowDOO.query),
|
||||
meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
private cashFlow = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const query = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const cashFlowAccountTransactions =
|
||||
await this.cashflowAccountTransactions.cashflowAccountTransactions(
|
||||
tenantId,
|
||||
query
|
||||
);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
// case 'application/json+table':
|
||||
// return res
|
||||
// .status(200)
|
||||
// .send(this.transformToTableRows(cashFlow, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(cashFlowAccountTransactions));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches the service errors.
|
||||
* @param {Error} error - Error.
|
||||
* @param {Request} req - Request.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'ACCOUNT_ID_HAS_INVALID_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'The given account id should be cash, bank or credit card type.',
|
||||
{
|
||||
errors: [{ type: 'ACCOUNT_ID_HAS_INVALID_TYPE', code: 200 }],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ICustomerBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import CustomerBalanceSummary from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import CustomerBalanceSummaryTableRows from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
customerBalanceSummaryService: CustomerBalanceSummary;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CUSTOMERS_SUMMARY_BALANCE,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.customerBalanceSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
// As date.
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Percentages.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters none-zero or none-transactions.
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to table rows.
|
||||
* @param {ICustomerBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId,
|
||||
{ data, query }: ICustomerBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableRows = new CustomerBalanceSummaryTableRows(data, query, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
columns: tableRows.tableColumns(),
|
||||
data: tableRows.tableRows(),
|
||||
},
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the balance summary statement to raw json.
|
||||
* @param {ICustomerBalanceSummaryStatement} customerBalance -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ICustomerBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async customerBalanceSummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const customerBalanceSummary =
|
||||
await this.customerBalanceSummaryService.customerBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, customerBalanceSummary));
|
||||
case 'application/json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(customerBalanceSummary));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import GeneralLedgerService from '@/services/FinancialStatements/GeneralLedger/GeneralLedgerService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class GeneralLedgerReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
generalLedgetService: GeneralLedgerService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.generalLedger.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('basis').optional(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
|
||||
query('accounts_ids').optional().isArray({ min: 1 }),
|
||||
query('accounts_ids.*').isInt().toInt(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async generalLedger(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.generalLedgetService.generalLedger(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import InventoryDetailsService from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsService';
|
||||
import InventoryDetailsTable from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryDetailsController extends BaseController {
|
||||
@Inject()
|
||||
inventoryDetailsService: InventoryDetailsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_INVENTORY_ITEM_DETAILS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inventoryDetails.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by warehouses.
|
||||
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('warehouses_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(inventoryDetails) {
|
||||
const { data, query, meta } = inventoryDetails;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
*/
|
||||
private transformToTableRows(inventoryDetails, tenantId: number) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const inventoryDetailsTable = new InventoryDetailsTable(
|
||||
inventoryDetails,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: inventoryDetailsTable.tableData(),
|
||||
columns: inventoryDetailsTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(inventoryDetails.query),
|
||||
meta: this.transfromToResponse(inventoryDetails.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async inventoryDetails(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const inventoryDetails =
|
||||
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(inventoryDetails, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(inventoryDetails));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import InventoryValuationService from '@/services/FinancialStatements/InventoryValuationSheet/InventoryValuationSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryValuationReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
inventoryValuationService: InventoryValuationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.inventoryValuation.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
query('none_zero').default(false).isBoolean().toBoolean(),
|
||||
query('only_active').default(false).isBoolean().toBoolean(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by warehouses.
|
||||
query('warehouses_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('warehouses_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async inventoryValuation(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.inventoryValuationService.inventoryValuationSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { castArray } from 'lodash';
|
||||
import { query, oneOf } from 'express-validator';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import JournalSheetService from '@/services/FinancialStatements/JournalSheet/JournalSheetService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class JournalSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
journalService: JournalSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_JOURNAL, AbilitySubject.Report),
|
||||
this.journalValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.journal.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get journalValidationSchema() {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('transaction_type').optional().trim().escape(),
|
||||
query('transaction_id').optional().isInt().toInt(),
|
||||
oneOf(
|
||||
[
|
||||
query('account_ids').optional().isArray({ min: 1 }),
|
||||
query('account_ids.*').optional().isNumeric().toInt(),
|
||||
],
|
||||
[query('account_ids').optional().isNumeric().toInt()]
|
||||
),
|
||||
query('from_range').optional().isNumeric().toInt(),
|
||||
query('to_range').optional().isNumeric().toInt(),
|
||||
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ledger report of the given account.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async journal(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.journalService.journalSheet(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import ProfitLossSheetService from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import { ProfitLossSheetTable } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
@Service()
|
||||
export default class ProfitLossSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
profitLossSheetService: ProfitLossSheetService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ReportsAction.READ_PROFIT_LOSS, AbilitySubject.Report),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.profitLossSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
|
||||
query('from_date').optional().isISO8601().toDate(),
|
||||
query('to_date').optional().isISO8601().toDate(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
query('accounts_ids').isArray().optional(),
|
||||
query('accounts_ids.*').isNumeric().toInt(),
|
||||
|
||||
query('display_columns_type').optional().isIn(['total', 'date_periods']),
|
||||
query('display_columns_by')
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isIn(['year', 'month', 'week', 'day', 'quarter']),
|
||||
|
||||
// Percentage options.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
query('percentage_row').optional().isBoolean().toBoolean(),
|
||||
query('percentage_expense').optional().isBoolean().toBoolean(),
|
||||
query('percentage_income').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Camparsion periods periods.
|
||||
query('previous_period').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_period_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
// Camparsion periods periods.
|
||||
query('previous_year').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_amount_change').optional().isBoolean().toBoolean(),
|
||||
query('previous_year_percentage_change')
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve profit/loss financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async profitLossSheet(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.profitLossSheetService.profitLossSheet(tenantId, filter);
|
||||
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const table = new ProfitLossSheetTable(data, query, i18n);
|
||||
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send({
|
||||
data,
|
||||
query,
|
||||
meta,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { query } from 'express-validator';
|
||||
import {
|
||||
NextFunction,
|
||||
Router,
|
||||
Request,
|
||||
Response,
|
||||
ValidationChain,
|
||||
} from 'express';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import {
|
||||
ICashFlowStatementDOO,
|
||||
AbilitySubject,
|
||||
ReportsAction,
|
||||
IProjectProfitabilitySummaryPOJO,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { ProjectProfitabilitySummaryTable } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryTable';
|
||||
import { ProjectProfitabilitySummaryService } from '@/services/FinancialStatements/ProjectProfitabilitySummary/ProjectProfitabilitySummaryService';
|
||||
|
||||
@Service()
|
||||
export default class ProjectProfitabilityController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
private projectProfitabilityService: ProjectProfitabilitySummaryService;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_PROJECT_PROFITABILITY_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.projectProfitabilitySummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Balance sheet validation schecma.
|
||||
* @returns {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional(),
|
||||
query('to_date').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by projects.
|
||||
query('products_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('products_ids.*').isNumeric().toInt(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cashflow statment to json response.
|
||||
* @param {ICashFlowStatement} cashFlow -
|
||||
*/
|
||||
private transformJsonResponse(projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO) {
|
||||
const { data, query, meta } = projectProfitabilityPOJO;
|
||||
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
projectProfitabilityPOJO: IProjectProfitabilitySummaryPOJO,
|
||||
tenantId: number
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const projectProfitabilityTable = new ProjectProfitabilitySummaryTable(
|
||||
projectProfitabilityPOJO.data,
|
||||
i18n
|
||||
);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: projectProfitabilityTable.tableData(),
|
||||
columns: projectProfitabilityTable.tableColumns(),
|
||||
},
|
||||
query: this.transfromToResponse(projectProfitabilityPOJO.query),
|
||||
// meta: this.transfromToResponse(cashFlowDOO.meta),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the cash flow statment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns {Response}
|
||||
*/
|
||||
async projectProfitabilitySummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const projectProfitability =
|
||||
await this.projectProfitabilityService.projectProfitabilitySummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(projectProfitability, tenantId));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformJsonResponse(projectProfitability));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class PurchasesByItemReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
purchasesByItemsService: PurchasesByItemsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_PURCHASES_BY_ITEMS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
// Filter items.
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters items.
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('only_active').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Specific items.
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import moment from 'moment';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import SalesByItemsReportService from '@/services/FinancialStatements/SalesByItems/SalesByItemsService';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class SalesByItemsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
salesByItemsService: SalesByItemsReportService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_BY_ITEMS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.purchasesByItems.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
// Specific items.
|
||||
query('items_ids').optional().isArray(),
|
||||
query('items_ids.*').optional().isInt().toInt(),
|
||||
|
||||
// Number format.
|
||||
query('number_format.no_cents').optional().isBoolean().toBoolean(),
|
||||
query('number_format.divide_1000').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters items.
|
||||
query('none_transactions').default(true).isBoolean().toBoolean(),
|
||||
query('only_active').default(false).isBoolean().toBoolean(),
|
||||
|
||||
// Order by.
|
||||
query('orderBy').optional().isIn(['created_at', 'name', 'code']),
|
||||
query('order').optional().isIn(['desc', 'asc']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the general ledger financial statement.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
async purchasesByItems(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, query, meta } = await this.salesByItemsService.salesByItems(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
meta: this.transfromToResponse(meta),
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByCustomersStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByCustomersService from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersService';
|
||||
import TransactionsByCustomersTableRows from '@/services/FinancialStatements/TransactionsByCustomer/TransactionsByCustomersTableRows';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class TransactionsByCustomersReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByCustomersService: TransactionsByCustomersService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_CUSTOMERS_TRANSACTIONS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByCustomers.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
private get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Customers ids.
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to table rows response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transformToTableResponse(customersTransactions, tenantId) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByCustomersTableRows(
|
||||
customersTransactions,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the statement to json response.
|
||||
* @param {ITransactionsByCustomersStatement} statement -
|
||||
*/
|
||||
private transfromToJsonResponse(
|
||||
data,
|
||||
columns
|
||||
): ITransactionsByCustomersStatement {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByCustomers(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByCustomersService.transactionsByCustomers(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'json':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transfromToJsonResponse(report.data, report.columns));
|
||||
case 'application/json+table':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableResponse(report.data, tenantId));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import TransactionsByReferenceService from '@/services/FinancialStatements/TransactionsByReference';
|
||||
import { ITransactionsByReferenceTransaction } from '@/interfaces';
|
||||
@Service()
|
||||
export default class TransactionsByReferenceController extends BaseController {
|
||||
@Inject()
|
||||
private transactionsByReferenceService: TransactionsByReferenceService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.transactionsByReference.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
query('reference_id').exists().isInt(),
|
||||
query('reference_type').exists().isString(),
|
||||
|
||||
query('number_format.precision')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: 5 })
|
||||
.toInt(),
|
||||
query('number_format.divide_on_1000').optional().isBoolean().toBoolean(),
|
||||
query('number_format.negative_format')
|
||||
.optional()
|
||||
.isIn(['parentheses', 'mines'])
|
||||
.trim()
|
||||
.escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve transactions by the given reference type and id.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response.
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public async transactionsByReference(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const data =
|
||||
await this.transactionsByReferenceService.getTransactionsByReference(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(data.transactions));
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the given report transaction to json response.
|
||||
* @param transactions
|
||||
* @returns
|
||||
*/
|
||||
private transformToJsonResponse(
|
||||
transactions: ITransactionsByReferenceTransaction[]
|
||||
) {
|
||||
return {
|
||||
transactions: this.transfromToResponse(transactions),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import TransactionsByVendorsTableRows from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorTableRows';
|
||||
import TransactionsByVendorsService from '@/services/FinancialStatements/TransactionsByVendor/TransactionsByVendorService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
ITransactionsByVendorsStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
export default class TransactionsByVendorsReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
transactionsByVendorsService: TransactionsByVendorsService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_VENDORS_TRANSACTIONS,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.transactionsByVendors.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(tenantId: number, transactions: any[]) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const table = new TransactionsByVendorsTableRows(transactions, i18n);
|
||||
|
||||
return {
|
||||
table: {
|
||||
data: table.tableRows(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to json response.
|
||||
* @param {ITransactionsByVendorsStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
query,
|
||||
}: ITransactionsByVendorsStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve payable aging summary report.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async transactionsByVendors(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const report =
|
||||
await this.transactionsByVendorsService.transactionsByVendors(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, report.data));
|
||||
case 'json':
|
||||
default:
|
||||
return res.status(200).send(this.transformToJsonResponse(report));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { query, ValidationChain } from 'express-validator';
|
||||
import { castArray } from 'lodash';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import TrialBalanceSheetService from '@/services/FinancialStatements/TrialBalanceSheet/TrialBalanceSheetService';
|
||||
import BaseFinancialReportController from './BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class TrialBalanceSheetController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
trialBalanceSheetService: TrialBalanceSheetService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_TRIAL_BALANCE_SHEET,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.trialBalanceSheetValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.trialBalanceSheet.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get trialBalanceSheetValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('basis').optional(),
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
query('account_ids').isArray().optional(),
|
||||
query('account_ids.*').isNumeric().toInt(),
|
||||
query('basis').optional(),
|
||||
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
query('only_active').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
query('branches_ids').optional().toArray().isArray({ min: 1 }),
|
||||
query('branches_ids.*').isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the trial balance sheet.
|
||||
*/
|
||||
public async trialBalanceSheet(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, settings } = req;
|
||||
let filter = this.matchedQueryData(req);
|
||||
|
||||
filter = {
|
||||
...filter,
|
||||
accountsIds: castArray(filter.accountsIds),
|
||||
};
|
||||
|
||||
try {
|
||||
const { data, query, meta } =
|
||||
await this.trialBalanceSheetService.trialBalanceSheet(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import VendorBalanceSummaryTableRows from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryTableRows';
|
||||
import VendorBalanceSummaryService from '@/services/FinancialStatements/VendorBalanceSummary/VendorBalanceSummaryService';
|
||||
import {
|
||||
AbilitySubject,
|
||||
IVendorBalanceSummaryStatement,
|
||||
ReportsAction,
|
||||
} from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
export default class VendorBalanceSummaryReportController extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
vendorBalanceSummaryService: VendorBalanceSummaryService;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_VENDORS_SUMMARY_BALANCE,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.vendorBalanceSummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
// Percentage columns.
|
||||
query('percentage_column').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Filters none-zero or none-transactions.
|
||||
query('none_zero').optional().isBoolean().toBoolean(),
|
||||
query('none_transactions').optional().isBoolean().toBoolean(),
|
||||
|
||||
// Vendors ids.
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').exists().isInt().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to table rows.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToTableRows(
|
||||
tenantId: number,
|
||||
{ data, query }: IVendorBalanceSummaryStatement
|
||||
) {
|
||||
const i18n = this.tenancy.i18n(tenantId);
|
||||
const tableData = new VendorBalanceSummaryTableRows(
|
||||
data,
|
||||
query,
|
||||
i18n
|
||||
);
|
||||
return {
|
||||
table: {
|
||||
columns: tableData.tableColumns(),
|
||||
data: tableData.tableRows(),
|
||||
},
|
||||
query,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the report statement to raw json.
|
||||
* @param {IVendorBalanceSummaryStatement} statement -
|
||||
*/
|
||||
private transformToJsonResponse({
|
||||
data,
|
||||
columns,
|
||||
}: IVendorBalanceSummaryStatement) {
|
||||
return {
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve vendors balance summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async vendorBalanceSummary(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, settings } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const vendorBalanceSummary =
|
||||
await this.vendorBalanceSummaryService.vendorBalanceSummary(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToTableRows(tenantId, vendorBalanceSummary));
|
||||
case 'json':
|
||||
default:
|
||||
return res
|
||||
.status(200)
|
||||
.send(this.transformToJsonResponse(vendorBalanceSummary));
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import BaseController from '../BaseController';
|
||||
import { InventoryCostApplication } from '@/services/Inventory/InventoryCostApplication';
|
||||
|
||||
@Service()
|
||||
export class InventoryItemsCostController extends BaseController {
|
||||
@Inject()
|
||||
private inventoryItemCost: InventoryCostApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/items-cost',
|
||||
[
|
||||
query('date').exists().isISO8601().toDate(),
|
||||
|
||||
query('items_ids').exists().isArray({ min: 1 }),
|
||||
query('items_ids.*').exists().isInt().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItemsCosts)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given items costs.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
public getItemsCosts = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const itemsCostQueryDTO = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const costs = await this.inventoryItemCost.getItemsInventoryValuationList(
|
||||
tenantId,
|
||||
itemsCostQueryDTO.itemsIds,
|
||||
itemsCostQueryDTO.date
|
||||
);
|
||||
return res.status(200).send({ costs });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, query, param } from 'express-validator';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '../BaseController';
|
||||
import InventoryAdjustmentService from '@/services/Inventory/InventoryAdjustmentService';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { AbilitySubject, InventoryAdjustmentAction } from '@/interfaces';
|
||||
import CheckPolicies from '../../middleware/CheckPolicies';
|
||||
|
||||
@Service()
|
||||
export default class InventoryAdjustmentsController extends BaseController {
|
||||
@Inject()
|
||||
inventoryAdjustmentService: InventoryAdjustmentService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.EDIT,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.publishInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.DELETE,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/quick',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.CREATE,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
this.validatateQuickAdjustment,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.createQuickInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.VIEW,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[param('id').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getInventoryAdjustment.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
InventoryAdjustmentAction.VIEW,
|
||||
AbilitySubject.InventoryAdjustment
|
||||
),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getInventoryAdjustments.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate list query schema
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick inventory adjustment validation schema.
|
||||
*/
|
||||
get validatateQuickAdjustment() {
|
||||
return [
|
||||
check('date').exists().isISO8601(),
|
||||
check('type')
|
||||
.exists()
|
||||
.isIn(['increment', 'decrement', 'value_adjustment']),
|
||||
check('reference_no').exists(),
|
||||
check('adjustment_account_id').exists().isInt().toInt(),
|
||||
check('reason').exists().isString().exists(),
|
||||
check('description').optional().isString(),
|
||||
check('item_id').exists().isInt().toInt(),
|
||||
check('quantity')
|
||||
.if(check('type').exists().isIn(['increment', 'decrement']))
|
||||
.exists()
|
||||
.isInt()
|
||||
.toInt(),
|
||||
check('cost')
|
||||
.if(check('type').exists().isIn(['increment']))
|
||||
.exists()
|
||||
.isFloat()
|
||||
.toInt(),
|
||||
check('publish').default(false).isBoolean().toBoolean(),
|
||||
|
||||
check('warehouse_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a quick inventory adjustment.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async createQuickInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId, user } = req;
|
||||
const quickInventoryAdjustment = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const inventoryAdjustment =
|
||||
await this.inventoryAdjustmentService.createQuickAdjustment(
|
||||
tenantId,
|
||||
quickInventoryAdjustment,
|
||||
user
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: inventoryAdjustment.id,
|
||||
message: 'The inventory adjustment has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given inventory adjustment transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async deleteInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inventoryAdjustmentService.deleteInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: adjustmentId,
|
||||
message: 'The inventory adjustment has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the given inventory adjustment transaction.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async publishInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inventoryAdjustmentService.publishInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: adjustmentId,
|
||||
message: 'The inventory adjustment has been published successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the specific inventory adjustment transaction of the given id.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async getInventoryAdjustment(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { id: adjustmentId } = req.params;
|
||||
|
||||
try {
|
||||
const inventoryAdjustment =
|
||||
await this.inventoryAdjustmentService.getInventoryAdjustment(
|
||||
tenantId,
|
||||
adjustmentId
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(inventoryAdjustment),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the inventory adjustments paginated list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getInventoryAdjustments(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
columnSortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
filterRoles: [],
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { pagination, inventoryAdjustments } =
|
||||
await this.inventoryAdjustmentService.getInventoryAdjustments(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
inventoy_adjustments: inventoryAdjustments,
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'INVENTORY_ADJUSTMENT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVENTORY_ADJUSTMENT.NOT.FOUND',
|
||||
code: 100,
|
||||
message: 'The inventory adjustment not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'account_not_found') {
|
||||
return res.boom.notFound('The given account not found.', {
|
||||
errors: [{ type: 'ACCOUNT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_SHOULD_BE_INVENTORY_TYPE') {
|
||||
return res.boom.badRequest(
|
||||
'You could not make adjustment on item has no inventory type.',
|
||||
{ errors: [{ type: 'ITEM_SHOULD_BE_INVENTORY_TYPE', code: 300 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{ type: 'INVENTORY_ADJUSTMENT_ALREADY_PUBLISHED', code: 400 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4900,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
268
packages/server/src/api/controllers/InviteUsers.ts
Normal file
268
packages/server/src/api/controllers/InviteUsers.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, body, param } from 'express-validator';
|
||||
import { IInviteUserInput } from '@/interfaces';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from './BaseController';
|
||||
import InviteTenantUserService from '@/services/InviteUsers/TenantInviteUser';
|
||||
import AcceptInviteUserService from '@/services/InviteUsers/AcceptInviteUser';
|
||||
|
||||
@Service()
|
||||
export default class InviteUsersController extends BaseController {
|
||||
@Inject()
|
||||
inviteUsersService: InviteTenantUserService;
|
||||
|
||||
@Inject()
|
||||
acceptInviteService: AcceptInviteUserService;
|
||||
|
||||
/**
|
||||
* Routes that require authentication.
|
||||
*/
|
||||
authRouter() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/send',
|
||||
[
|
||||
body('email').exists().trim().escape(),
|
||||
body('role_id').exists().isNumeric().toInt(),
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.sendInvite.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
router.post(
|
||||
'/resend/:userId',
|
||||
[param('userId').exists().isNumeric().toInt()],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.resendInvite.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Routes that non-required authentication.
|
||||
*/
|
||||
nonAuthRouter() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/accept/:token',
|
||||
[...this.inviteUserDTO],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.accept.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
router.get(
|
||||
'/invited/:token',
|
||||
[param('token').exists().trim().escape()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.invited.bind(this)),
|
||||
this.handleServicesError
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite DTO schema validation.
|
||||
*/
|
||||
get inviteUserDTO() {
|
||||
return [
|
||||
check('first_name').exists().trim().escape(),
|
||||
check('last_name').exists().trim().escape(),
|
||||
check('phone_number').exists().trim().escape(),
|
||||
check('password').exists().trim().escape(),
|
||||
param('token').exists().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to the authorized user organization.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async sendInvite(req: Request, res: Response, next: Function) {
|
||||
const sendInviteDTO = this.matchedBodyData(req);
|
||||
const { tenantId } = req;
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const { invite } = await this.inviteUsersService.sendInvite(
|
||||
tenantId,
|
||||
sendInviteDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.SENT.SUCCESSFULLY',
|
||||
message: 'The invite has been sent to the given email.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resend the user invite.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
async resendInvite(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { userId } = req.params;
|
||||
|
||||
try {
|
||||
await this.inviteUsersService.resendInvite(tenantId, userId, user);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'INVITE.RESEND.SUCCESSFULLY',
|
||||
message: 'The invite has been sent to the given email.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept the inviation.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async accept(req: Request, res: Response, next: Function) {
|
||||
const inviteUserInput: IInviteUserInput = this.matchedBodyData(req, {
|
||||
locations: ['body'],
|
||||
includeOptionals: true,
|
||||
});
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
await this.acceptInviteService.acceptInvite(token, inviteUserInput);
|
||||
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'USER.INVITE.ACCEPTED',
|
||||
message: 'User invite has been accepted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the invite token is valid.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async invited(req: Request, res: Response, next: Function) {
|
||||
const { token } = req.params;
|
||||
|
||||
try {
|
||||
const { inviteToken, orgName } =
|
||||
await this.acceptInviteService.checkInvite(token);
|
||||
|
||||
return res.status(200).send({
|
||||
inviteToken: inviteToken.token,
|
||||
email: inviteToken.email,
|
||||
organizationName: orgName,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the service error.
|
||||
*/
|
||||
handleServicesError(error, req: Request, res: Response, next: Function) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'EMAIL_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EMAIL.ALREADY.EXISTS',
|
||||
code: 100,
|
||||
message: 'Email already exists in the users.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'EMAIL_ALREADY_INVITED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'EMAIL.ALREADY.INVITED',
|
||||
code: 200,
|
||||
message: 'Email already invited.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVITE_TOKEN_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVITE.TOKEN.INVALID',
|
||||
code: 300,
|
||||
message: 'Invite token is invalid, please try another one.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'PHONE_NUMBER_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'PHONE_NUMBER.EXISTS',
|
||||
code: 400,
|
||||
message:
|
||||
'Phone number is already invited, please try another unique one.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_RECENTLY_INVITED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'USER_RECENTLY_INVITED',
|
||||
code: 500,
|
||||
message:
|
||||
'This person was recently invited. No need to invite them again just yet.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ROLE_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ROLE_NOT_FOUND',
|
||||
code: 600,
|
||||
message: 'The given user role is not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'USER_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'USER_NOT_FOUND',
|
||||
code: 700,
|
||||
message: 'The given user is not found.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
311
packages/server/src/api/controllers/ItemCategories.ts
Normal file
311
packages/server/src/api/controllers/ItemCategories.ts
Normal file
@@ -0,0 +1,311 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import ItemCategoriesService from '@/services/ItemCategories/ItemCategoriesService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { IItemCategoryOTD } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
|
||||
@Service()
|
||||
export default class ItemsCategoriesController extends BaseController {
|
||||
@Inject()
|
||||
itemCategoriesService: ItemCategoriesService;
|
||||
|
||||
@Inject()
|
||||
dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/:id',
|
||||
[
|
||||
...this.categoryValidationSchema,
|
||||
...this.specificCategoryValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
[...this.categoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.newCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
[...this.specificCategoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteItem.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
[...this.specificCategoryValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getCategory.bind(this)),
|
||||
this.handlerServiceError
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
[...this.categoriesListValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getList.bind(this)),
|
||||
this.handlerServiceError,
|
||||
this.dynamicListService.handlerErrorsToResponse
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item category validation schema.
|
||||
*/
|
||||
get categoryValidationSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ min: 0, max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('sell_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('cost_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('inventory_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate items categories schema.
|
||||
*/
|
||||
get categoriesListValidationSchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().trim().escape().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate specific item category schema.
|
||||
*/
|
||||
get specificCategoryValidationSchema() {
|
||||
return [param('id').exists().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new item category.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { user, tenantId } = req;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.newItemCategory(
|
||||
tenantId,
|
||||
itemCategoryOTD,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategory.id,
|
||||
message: 'The item category has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit details of the given category item.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async editCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const itemCategoryOTD: IItemCategoryOTD = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.itemCategoriesService.editItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
itemCategoryOTD,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategoryId,
|
||||
message: 'The item category has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the give item category.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { id: itemCategoryId } = req.params;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
await this.itemCategoriesService.deleteItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: itemCategoryId,
|
||||
message: 'The item category has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of items.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId, user } = req;
|
||||
|
||||
const itemCategoriesFilter = {
|
||||
sortOrder: 'asc',
|
||||
columnSortBy: 'created_at',
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
itemCategories,
|
||||
filterMeta,
|
||||
} = await this.itemCategoriesService.getItemCategoriesList(
|
||||
tenantId,
|
||||
itemCategoriesFilter,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
item_categories: itemCategories,
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details of the given category.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @return {Response}
|
||||
*/
|
||||
async getCategory(req: Request, res: Response, next: NextFunction) {
|
||||
const itemCategoryId: number = req.params.id;
|
||||
const { tenantId, user } = req;
|
||||
|
||||
try {
|
||||
const itemCategory = await this.itemCategoriesService.getItemCategory(
|
||||
tenantId,
|
||||
itemCategoryId,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({ category: itemCategory });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service error.
|
||||
* @param {Error} error
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceError(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'CATEGORY_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORY_NOT_FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CATEGORIES_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_CATEGORIES_NOT_FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CATEGORY_NAME_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'CATEGORY_NAME_EXISTS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 400 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 600 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 700 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.CURRENT.ASSET', code: 900 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
522
packages/server/src/api/controllers/Items/Items.ts
Normal file
522
packages/server/src/api/controllers/Items/Items.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, param, query, ValidationChain } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { IItemDTO, ItemAction, AbilitySubject } from '@/interfaces';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckAbilities from '@/api/middleware/CheckPolicies';
|
||||
import { ItemsApplication } from '@/services/Items/ItemsApplication';
|
||||
|
||||
@Service()
|
||||
export default class ItemsController extends BaseController {
|
||||
@Inject()
|
||||
private itemsApplication: ItemsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckAbilities(ItemAction.CREATE, AbilitySubject.Item),
|
||||
this.validateItemSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.newItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/activate',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
this.validateSpecificItemSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.activateItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactivate',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.inactivateItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.EDIT, AbilitySubject.Item),
|
||||
[...this.validateItemSchema, ...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.editItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.DELETE, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.deleteItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
|
||||
[...this.validateSpecificItemSchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItem.bind(this)),
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckAbilities(ItemAction.VIEW, AbilitySubject.Item),
|
||||
[...this.validateListQuerySchema],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.getItemsList.bind(this)),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.handlerServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate item schema.
|
||||
*/
|
||||
get validateItemSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('type')
|
||||
.exists()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isIn(['service', 'non-inventory', 'inventory']),
|
||||
check('code')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
// Purchase attributes.
|
||||
check('purchasable').optional().isBoolean().toBoolean(),
|
||||
check('cost_price')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat()
|
||||
.if(check('purchasable').equals('true'))
|
||||
.exists(),
|
||||
check('cost_account_id').if(check('purchasable').equals('true')).exists(),
|
||||
check('cost_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
// Sell attributes.
|
||||
check('sellable').optional().isBoolean().toBoolean(),
|
||||
check('sell_price')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat()
|
||||
.if(check('sellable').equals('true'))
|
||||
.exists(),
|
||||
check('sell_account_id').if(check('sellable').equals('true')).exists(),
|
||||
check('sell_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('inventory_account_id')
|
||||
.if(check('type').equals('inventory'))
|
||||
.exists(),
|
||||
check('inventory_account_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('sell_description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('purchase_description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('category_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('note')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('active').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('media_ids').optional().isArray(),
|
||||
check('media_ids.*').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate specific item params schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
get validateSpecificItemSchema(): ValidationChain[] {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate list query schema.
|
||||
*/
|
||||
get validateListQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
query('inactive_mode').optional().isBoolean().toBoolean(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate autocomplete list query schema.
|
||||
*/
|
||||
get autocompleteQuerySchema() {
|
||||
return [
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('limit').optional().isNumeric().toInt(),
|
||||
|
||||
query('keyword').optional().isString().trim().escape(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given item details to the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async newItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemDTO: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const storedItem = await this.itemsApplication.createItem(tenantId, itemDTO);
|
||||
|
||||
return res.status(200).send({
|
||||
id: storedItem.id,
|
||||
message: 'The item has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the given item details on the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async editItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
const item: IItemDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.itemsApplication.editItem(tenantId, itemId, item);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given item.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async activateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.activateItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given item.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async inactivateItem(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const itemId: number = req.params.id;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.inactivateItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given item from the storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async deleteItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
await this.itemsApplication.deleteItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: itemId,
|
||||
message: 'The item has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve details the given item id.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @return {Response}
|
||||
*/
|
||||
async getItem(req: Request, res: Response, next: NextFunction) {
|
||||
const itemId: number = req.params.id;
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const item = await this.itemsApplication.getItem(tenantId, itemId);
|
||||
|
||||
return res.status(200).send({
|
||||
item: this.transfromToResponse(item),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve items datatable list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getItemsList(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
const filter = {
|
||||
sortOrder: 'DESC',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
inactiveMode: false,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
|
||||
try {
|
||||
const { items, pagination, filterMeta } =
|
||||
await this.itemsApplication.getItems(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
items: this.transfromToResponse(items),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS_NOT_FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CATEOGRY_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM_CATEGORY.NOT.FOUND', code: 140 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_NAME_EXISTS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM.NAME.ALREADY.EXISTS', code: 210 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_FOUMD') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.FOUND', code: 120 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'COST_ACCOUNT_NOT_COGS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'COST.ACCOUNT.NOT.COGS.TYPE', code: 220 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.FOUND', code: 130 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_FOUND') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.FOUND', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'SELL_ACCOUNT_NOT_INCOME') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'SELL.ACCOUNT.NOT.INCOME.TYPE', code: 230 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_NOT_INVENTORY') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVENTORY.ACCOUNT.NOT.INVENTORY.TYPE', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS', code: 310 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTINS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'ITEM_HAS_ASSOCIATED_TRANSACTINS', code: 320 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{ type: 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT', code: 330 },
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
message: 'Cannot change inventory item type',
|
||||
code: 340,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
message:
|
||||
'Cannot change item type to inventory with item has associated transactions.',
|
||||
code: 350,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'INVENTORY_ACCOUNT_CANNOT_MODIFIED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
message:
|
||||
'Cannot change item inventory account while the item has transactions.',
|
||||
code: 360,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_HAS_ASSOCIATED_TRANSACTIONS') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
code: 370,
|
||||
message:
|
||||
'Could not delete item that has associated transactions.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
137
packages/server/src/api/controllers/Items/ItemsTransactions.ts
Normal file
137
packages/server/src/api/controllers/Items/ItemsTransactions.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import ItemTransactionsService from '@/services/Items/ItemTransactionsService';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
@Service()
|
||||
export default class ItemTransactionsController extends BaseController {
|
||||
@Inject()
|
||||
itemTransactionsService: ItemTransactionsService;
|
||||
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/:id/transactions/invoices',
|
||||
this.asyncMiddleware(this.getItemInvoicesTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/bills',
|
||||
this.asyncMiddleware(this.getItemBillTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/estimates',
|
||||
this.asyncMiddleware(this.getItemEstimateTransactions)
|
||||
);
|
||||
router.get(
|
||||
'/:id/transactions/receipts',
|
||||
this.asyncMiddleware(this.getItemReceiptTransactions)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve item associated invoices transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemInvoicesTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: invoiceId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemInvoicesTransactions(
|
||||
tenantId,
|
||||
invoiceId
|
||||
);
|
||||
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item associated bills transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemBillTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: billId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemBillTransactions(
|
||||
tenantId,
|
||||
billId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve item associated estimates transactions.
|
||||
* @param {Request} req - Request object.
|
||||
* @param {Response} res - Response object.
|
||||
* @param {NextFunction} next - Next function.
|
||||
*/
|
||||
public getItemEstimateTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: estimateId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemEstimateTransactions(
|
||||
tenantId,
|
||||
estimateId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
*/
|
||||
public getItemReceiptTransactions = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: receiptId } = req.params;
|
||||
|
||||
try {
|
||||
const transactions =
|
||||
await this.itemTransactionsService.getItemReceiptTransactions(
|
||||
tenantId,
|
||||
receiptId
|
||||
);
|
||||
return res.status(200).send({ data: transactions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
17
packages/server/src/api/controllers/Items/index.ts
Normal file
17
packages/server/src/api/controllers/Items/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Container, Service } from 'typedi';
|
||||
import ItemsController from './Items';
|
||||
|
||||
import ItemTransactionsController from './ItemsTransactions';
|
||||
|
||||
@Service()
|
||||
export default class ItemsBaseController {
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ItemsController).router());
|
||||
router.use('/', Container.get(ItemTransactionsController).router());
|
||||
|
||||
return router;
|
||||
}
|
||||
}
|
||||
60
packages/server/src/api/controllers/Jobs.ts
Normal file
60
packages/server/src/api/controllers/Jobs.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import JobsService from '@/services/Jobs/JobsService';
|
||||
|
||||
@Service()
|
||||
export default class ItemsController extends BaseController {
|
||||
@Inject()
|
||||
jobsService: JobsService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get('/:id', this.getJob, this.handlerServiceErrors);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve job details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getJob = async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { id } = req.params;
|
||||
|
||||
try {
|
||||
const job = await this.jobsService.getJob(id);
|
||||
|
||||
return res.status(200).send({
|
||||
job: this.transfromToResponse(job),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handlerServiceErrors = (
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
477
packages/server/src/api/controllers/ManualJournals.ts
Normal file
477
packages/server/src/api/controllers/ManualJournals.ts
Normal file
@@ -0,0 +1,477 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import { check, param, query } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||
import { DATATYPES_LENGTH } from '@/data/DataTypes';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { AbilitySubject, ManualJournalAction } from '@/interfaces';
|
||||
import { ManualJournalsApplication } from '@/services/ManualJournals/ManualJournalsApplication';
|
||||
|
||||
@Service()
|
||||
export default class ManualJournalsController extends BaseController {
|
||||
@Inject()
|
||||
private manualJournalsApplication: ManualJournalsApplication;
|
||||
|
||||
@Inject()
|
||||
private dynamicListService: DynamicListingService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(ManualJournalAction.View, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalsListSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getManualJournalsList),
|
||||
this.dynamicListService.handlerErrorsToResponse,
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.View, AbilitySubject.ManualJournal),
|
||||
asyncMiddleware(this.getManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/publish',
|
||||
CheckPolicies(ManualJournalAction.Edit, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.publishManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.Edit, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalValidationSchema, ...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckPolicies(ManualJournalAction.Delete, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalParamSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteManualJournal),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/',
|
||||
CheckPolicies(ManualJournalAction.Create, AbilitySubject.ManualJournal),
|
||||
[...this.manualJournalValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.makeJournalEntries),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specific manual journal id param validation schema.
|
||||
*/
|
||||
get manualJournalParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journal DTO schema.
|
||||
*/
|
||||
get manualJournalValidationSchema() {
|
||||
return [
|
||||
check('date').exists().isISO8601(),
|
||||
check('currency_code').optional(),
|
||||
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
|
||||
|
||||
check('journal_number')
|
||||
.optional()
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('journal_type')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('reference')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('description')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.escape()
|
||||
.isLength({ max: DATATYPES_LENGTH.TEXT }),
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('publish').optional().isBoolean().toBoolean(),
|
||||
check('entries').isArray({ min: 2 }),
|
||||
check('entries.*.index')
|
||||
.exists()
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.credit')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat(),
|
||||
check('entries.*.debit')
|
||||
.optional({ nullable: true })
|
||||
.isFloat({ min: 0, max: DATATYPES_LENGTH.DECIMAL_13_3 })
|
||||
.toFloat(),
|
||||
check('entries.*.account_id')
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.note')
|
||||
.optional({ nullable: true })
|
||||
.isString()
|
||||
.isLength({ max: DATATYPES_LENGTH.STRING }),
|
||||
check('entries.*.contact_id')
|
||||
.optional({ nullable: true })
|
||||
.isInt({ max: DATATYPES_LENGTH.INT_10 })
|
||||
.toInt(),
|
||||
check('entries.*.branch_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.project_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual journals list validation schema.
|
||||
*/
|
||||
get manualJournalsListSchema() {
|
||||
return [
|
||||
query('page').optional().isNumeric().toInt(),
|
||||
query('page_size').optional().isNumeric().toInt(),
|
||||
query('custom_view_id').optional().isNumeric().toInt(),
|
||||
|
||||
query('column_sort_by').optional().trim().escape(),
|
||||
query('sort_order').optional().isIn(['desc', 'asc']),
|
||||
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
query('search_keyword').optional({ nullable: true }).isString().trim(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Make manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private makeJournalEntries = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const manualJournalDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { manualJournal } =
|
||||
await this.manualJournalsApplication.createManualJournal(
|
||||
tenantId,
|
||||
manualJournalDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournal.id,
|
||||
message: 'The manual journal has been created successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Edit the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private editManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
const manualJournalDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const { manualJournal } =
|
||||
await this.manualJournalsApplication.editManualJournal(
|
||||
tenantId,
|
||||
manualJournalId,
|
||||
manualJournalDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournal.id,
|
||||
message: 'The manual journal has been edited successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the given manual journal details.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private getManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
const manualJournal =
|
||||
await this.manualJournalsApplication.getManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
manual_journal: this.transfromToResponse(manualJournal),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private publishManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
await this.manualJournalsApplication.publishManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournalId,
|
||||
message: 'The manual journal has been published successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the given manual journal.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private deleteManualJournal = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId, user } = req;
|
||||
const { id: manualJournalId } = req.params;
|
||||
|
||||
try {
|
||||
await this.manualJournalsApplication.deleteManualJournal(
|
||||
tenantId,
|
||||
manualJournalId
|
||||
);
|
||||
return res.status(200).send({
|
||||
id: manualJournalId,
|
||||
message: 'Manual journal has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve manual journals list.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
getManualJournalsList = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
const filter = {
|
||||
sortOrder: 'desc',
|
||||
columnSortBy: 'created_at',
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
...this.matchedQueryData(req),
|
||||
};
|
||||
try {
|
||||
const { manualJournals, pagination, filterMeta } =
|
||||
await this.manualJournalsApplication.getManualJournals(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
manual_journals: this.transfromToResponse(manualJournals),
|
||||
pagination: this.transfromToResponse(pagination),
|
||||
filter_meta: this.transfromToResponse(filterMeta),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Catches all service errors.
|
||||
* @param error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
catchServiceErrors = (
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'manual_journal_not_found') {
|
||||
res.boom.badRequest('Manual journal not found.', {
|
||||
errors: [{ type: 'MANUAL.JOURNAL.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'credit_debit_not_equal_zero') {
|
||||
return res.boom.badRequest(
|
||||
'Credit and debit should not be equal zero.',
|
||||
{
|
||||
errors: [
|
||||
{
|
||||
type: 'CREDIT.DEBIT.SUMATION.SHOULD.NOT.EQUAL.ZERO',
|
||||
code: 200,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'credit_debit_not_equal') {
|
||||
return res.boom.badRequest('Credit and debit should be equal.', {
|
||||
errors: [{ type: 'CREDIT.DEBIT.NOT.EQUALS', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'acccounts_ids_not_found') {
|
||||
return res.boom.badRequest(
|
||||
'Journal entries some of accounts ids not exists.',
|
||||
{ errors: [{ type: 'ACCOUNTS.IDS.NOT.FOUND', code: 400 }] }
|
||||
);
|
||||
}
|
||||
if (error.errorType === 'journal_number_exists') {
|
||||
return res.boom.badRequest('Journal number should be unique.', {
|
||||
errors: [{ type: 'JOURNAL.NUMBER.ALREADY.EXISTS', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
|
||||
code: 600,
|
||||
meta: this.transfromToResponse(error.payload),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'CONTACTS_SHOULD_ASSIGN_WITH_VALID_ACCOUNT',
|
||||
code: 700,
|
||||
meta: this.transfromToResponse(error.payload),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'contacts_not_found') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [{ type: 'CONTACTS_NOT_FOUND', code: 800 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_ALREADY_PUBLISHED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [{ type: 'MANUAL_JOURNAL_ALREADY_PUBLISHED', code: 900 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_NO_REQUIRED') {
|
||||
return res.boom.badRequest('', {
|
||||
errors: [
|
||||
{
|
||||
type: 'MANUAL_JOURNAL_NO_REQUIRED',
|
||||
message: 'The manual journal number required.',
|
||||
code: 1000,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TRANSACTIONS_DATE_LOCKED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'TRANSACTIONS_DATE_LOCKED',
|
||||
code: 4000,
|
||||
data: { ...error.payload },
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType === 'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS',
|
||||
code: 1100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{ type: 'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID', code: 1200 },
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
212
packages/server/src/api/controllers/Media.ts
Normal file
212
packages/server/src/api/controllers/Media.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
param,
|
||||
query,
|
||||
check,
|
||||
} from 'express-validator';
|
||||
import { camelCase, upperFirst } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { IMediaLinkDTO } from '@/interfaces';
|
||||
import fs from 'fs';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseController from './BaseController';
|
||||
import MediaService from '@/services/Media/MediaService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
@Service()
|
||||
export default class MediaController extends BaseController {
|
||||
@Inject()
|
||||
mediaService: MediaService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post('/upload', [
|
||||
...this.uploadValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.uploadMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.post('/:id/link', [
|
||||
...this.mediaIdParamSchema,
|
||||
...this.linkValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.linkMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.delete('/', [
|
||||
...this.deleteValidationSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
router.get('/:id', [
|
||||
...this.mediaIdParamSchema,
|
||||
],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getMedia.bind(this)),
|
||||
this.handlerServiceErrors,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
get uploadValidationSchema() {
|
||||
return [
|
||||
// check('attachment'),
|
||||
check('model_name').optional().trim().escape(),
|
||||
check('model_id').optional().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get linkValidationSchema() {
|
||||
return [
|
||||
check('model_name').exists().trim().escape(),
|
||||
check('model_id').exists().isNumeric().toInt(),
|
||||
]
|
||||
}
|
||||
|
||||
get deleteValidationSchema() {
|
||||
return [
|
||||
query('ids').exists().isArray(),
|
||||
query('ids.*').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
get mediaIdParamSchema() {
|
||||
return [
|
||||
param('id').exists().isNumeric().toInt(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all or the given attachment ids.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async getMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: mediaId } = req.params;
|
||||
|
||||
try {
|
||||
const media = await this.mediaService.getMedia(tenantId, mediaId);
|
||||
return res.status(200).send({ media });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads media.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async uploadMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { attachment } = req.files
|
||||
|
||||
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
|
||||
const modelName = linkMediaDTO.modelName
|
||||
? upperFirst(camelCase(linkMediaDTO.modelName)) : '';
|
||||
|
||||
try {
|
||||
const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId);
|
||||
return res.status(200).send({ media_id: media.id });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given attachment ids from file system and database.
|
||||
* @param {Request} req -
|
||||
* @param {Response} req -
|
||||
* @param {NextFunction} req -
|
||||
*/
|
||||
async deleteMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { ids: mediaIds } = req.query;
|
||||
|
||||
try {
|
||||
await this.mediaService.deleteMedia(tenantId, mediaIds);
|
||||
return res.status(200).send({
|
||||
media_ids: mediaIds
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given media to the specific resource model.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async linkMedia(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { id: mediaId } = req.params;
|
||||
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
|
||||
const modelName = upperFirst(camelCase(linkMediaDTO.modelName));
|
||||
|
||||
try {
|
||||
await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName);
|
||||
return res.status(200).send({ media_id: mediaId });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'MINETYPE_NOT_SUPPORTED') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MODEL_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }]
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_IDS_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'MEDIA_LINK_EXISTS') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
41
packages/server/src/api/controllers/Miscellaneous/index.ts
Normal file
41
packages/server/src/api/controllers/Miscellaneous/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import MiscService from '@/services/Miscellaneous/MiscService';
|
||||
import DateFormatsService from '@/services/Miscellaneous/DateFormats';
|
||||
|
||||
@Service()
|
||||
export default class MiscController extends BaseController {
|
||||
@Inject()
|
||||
dateFormatsService: DateFormatsService;
|
||||
|
||||
/**
|
||||
* Express router.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/date_formats',
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.dateFormats.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve date formats options.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
dateFormats(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const dateFormats = this.dateFormatsService.getDateFormats();
|
||||
|
||||
return res.status(200).send({ data: dateFormats });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
199
packages/server/src/api/controllers/Organization.ts
Normal file
199
packages/server/src/api/controllers/Organization.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import moment from 'moment-timezone';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { check, ValidationChain } from 'express-validator';
|
||||
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import JWTAuth from '@/api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from '@/api/middleware/TenancyMiddleware';
|
||||
import SubscriptionMiddleware from '@/api/middleware/SubscriptionMiddleware';
|
||||
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
|
||||
import OrganizationService from '@/services/Organization/OrganizationService';
|
||||
import {
|
||||
ACCEPTED_CURRENCIES,
|
||||
MONTHS,
|
||||
ACCEPTED_LOCALES,
|
||||
} from '@/services/Organization/constants';
|
||||
import { DATE_FORMATS } from '@/services/Miscellaneous/DateFormats/constants';
|
||||
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
|
||||
const ACCEPTED_LOCATIONS = ['libya'];
|
||||
|
||||
@Service()
|
||||
export default class OrganizationController extends BaseController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
// Should before build tenant database the user be authorized and
|
||||
// most important than that, should be subscribed to any plan.
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
|
||||
router.use('/build', SubscriptionMiddleware('main'));
|
||||
router.post(
|
||||
'/build',
|
||||
this.organizationValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.build.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
router.put(
|
||||
'/',
|
||||
this.organizationValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.updateOrganization.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
asyncMiddleware(this.currentOrganization.bind(this)),
|
||||
this.handleServiceErrors.bind(this)
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization setup schema.
|
||||
* @return {ValidationChain[]}
|
||||
*/
|
||||
private get organizationValidationSchema(): ValidationChain[] {
|
||||
return [
|
||||
check('name').exists().trim(),
|
||||
check('industry').optional().isString(),
|
||||
check('location').exists().isString().isIn(ACCEPTED_LOCATIONS),
|
||||
check('base_currency').exists().isIn(ACCEPTED_CURRENCIES),
|
||||
check('timezone').exists().isIn(moment.tz.names()),
|
||||
check('fiscal_year').exists().isIn(MONTHS),
|
||||
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
|
||||
check('date_format').optional().isIn(DATE_FORMATS),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds tenant database and migrate database schema.
|
||||
* @param {Request} req - Express request.
|
||||
* @param {Response} res - Express response.
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async build(req: Request, res: Response, next: Function) {
|
||||
const { tenantId, user } = req;
|
||||
const buildDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const result = await this.organizationService.buildRunJob(
|
||||
tenantId,
|
||||
buildDTO,
|
||||
user
|
||||
);
|
||||
return res.status(200).send({
|
||||
type: 'success',
|
||||
code: 'ORGANIZATION.DATABASE.INITIALIZED',
|
||||
message: 'The organization database has been initialized.',
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the current organization of the associated authenticated user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async currentOrganization(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const organization = await this.organizationService.currentOrganization(
|
||||
tenantId
|
||||
);
|
||||
return res.status(200).send({
|
||||
organization: this.transfromToResponse(organization),
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the organization information.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private async updateOrganization(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const tenantDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
await this.organizationService.updateOrganization(tenantId, tenantDTO);
|
||||
|
||||
return res.status(200).send(
|
||||
this.transfromToResponse({
|
||||
tenantId,
|
||||
message: 'Organization information has been updated successfully.',
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(
|
||||
error: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'tenant_not_found') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT.NOT.FOUND', code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_ALREADY_BUILT') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_ALREADY_BUILT', code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_IS_BUILDING') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'TENANT_IS_BUILDING', code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'BASE_CURRENCY_MUTATE_LOCKED') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'BASE_CURRENCY_MUTATE_LOCKED', code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
124
packages/server/src/api/controllers/OrganizationDashboard.ts
Normal file
124
packages/server/src/api/controllers/OrganizationDashboard.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Request, Response, Router, NextFunction } from 'express';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import OrganizationService from '../../services/Organization/OrganizationService';
|
||||
import OrganizationUpgrade from '../../services/Organization/OrganizationUpgrade';
|
||||
import { ServiceError } from '../../exceptions';
|
||||
|
||||
@Service()
|
||||
export default class OrganizationDashboardController extends BaseController {
|
||||
@Inject()
|
||||
organizationService: OrganizationService;
|
||||
|
||||
@Inject()
|
||||
organizationUpgrade: OrganizationUpgrade;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/base_currency_mutate',
|
||||
this.baseCurrencyMutateAbility.bind(this)
|
||||
);
|
||||
router.post(
|
||||
'/upgrade',
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.upgradeOrganization),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
private async baseCurrencyMutateAbility(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: Function
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const abilities =
|
||||
await this.organizationService.mutateBaseCurrencyAbility(tenantId);
|
||||
|
||||
return res.status(200).send({ abilities });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upgrade the authenticated organization.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
* @returns {Response}
|
||||
*/
|
||||
public upgradeOrganization = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
// Upgrade organization database.
|
||||
const { jobId } = await this.organizationUpgrade.upgrade(tenantId);
|
||||
|
||||
return res.status(200).send({
|
||||
job_id: jobId,
|
||||
message: 'The organization has been upgraded successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
private handleServiceErrors = (
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'TENANT_DATABASE_UPGRADED') {
|
||||
return res.status(400).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TENANT_DATABASE_UPGRADED',
|
||||
message: 'Organization database is already upgraded.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'TENANT_UPGRADE_IS_RUNNING') {
|
||||
return res.status(200).send({
|
||||
errors: [
|
||||
{
|
||||
type: 'TENANT_UPGRADE_IS_RUNNING',
|
||||
message: 'Organization database upgrade is running.',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
};
|
||||
}
|
||||
28
packages/server/src/api/controllers/Ping.ts
Normal file
28
packages/server/src/api/controllers/Ping.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
|
||||
export default class Ping {
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
this.ping,
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the ping request.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async ping(req: Request, res: Response)
|
||||
{
|
||||
return res.status(200).send({
|
||||
server: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user