mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-19 22:30: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