Compare commits
80 Commits
fix-spelli
...
@bigcapita
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04b16a7d2f | ||
|
|
fd27136274 | ||
|
|
951e360405 | ||
|
|
d2242b7744 | ||
|
|
4e9b0ae24f | ||
|
|
e5ffb5661a | ||
|
|
9eaff0785b | ||
|
|
b3a97ed5d5 | ||
|
|
1aaa65edd2 | ||
|
|
efebf424d1 | ||
|
|
597973d5d1 | ||
|
|
ee6bc822c9 | ||
|
|
07e78ebd6a | ||
|
|
8c04a6205b | ||
|
|
d54ea68d46 | ||
|
|
f42c8031c2 | ||
|
|
5261d332b7 | ||
|
|
f1e7d0fc44 | ||
|
|
1d1049043e | ||
|
|
1148fef9ad | ||
|
|
92ac0dbd25 | ||
|
|
ce41845bd7 | ||
|
|
eaf72d1608 | ||
|
|
ac336f9878 | ||
|
|
746c80c564 | ||
|
|
601434b107 | ||
|
|
5aaa33e585 | ||
|
|
d48d864a5f | ||
|
|
453df2ac4e | ||
|
|
73ceeaee46 | ||
|
|
1b4b656419 | ||
|
|
df823c0bfe | ||
|
|
e91d7b0cff | ||
|
|
aa52e7d02c | ||
|
|
4e53d08497 | ||
|
|
2356921f27 | ||
|
|
a0a5d00be3 | ||
|
|
fbd74c559b | ||
|
|
8a64198433 | ||
|
|
b98b73ad98 | ||
|
|
6abae43c6f | ||
|
|
7657337c4f | ||
|
|
983ceb5cc6 | ||
|
|
ac072d29fc | ||
|
|
17e055db5e | ||
|
|
b49a021506 | ||
|
|
b49b45fb43 | ||
|
|
bb7cf41e3e | ||
|
|
801ea5dfdb | ||
|
|
eb03a38553 | ||
|
|
0852feecbf | ||
|
|
54dcde657f | ||
|
|
5bb95eeb1a | ||
|
|
6baec8dd96 | ||
|
|
6535424d0f | ||
|
|
09d73db20f | ||
|
|
7f746b96c8 | ||
|
|
ed2bca6b74 | ||
|
|
f9d5a3c69a | ||
|
|
84445d4bac | ||
|
|
75d8864aae | ||
|
|
cb6dab08d8 | ||
|
|
5112ef9b64 | ||
|
|
a630e8a612 | ||
|
|
4df63561cf | ||
|
|
c7a3bac44c | ||
|
|
251c54be60 | ||
|
|
5dec4a7df0 | ||
|
|
0d57ca88bf | ||
|
|
b9be83dc2b | ||
|
|
321de4d327 | ||
|
|
4e66d1ac98 | ||
|
|
b5fe5a8bcb | ||
|
|
d1121f0b81 | ||
|
|
a7644e6481 | ||
|
|
d6f56568a3 | ||
|
|
04d134806b | ||
|
|
339559d830 | ||
|
|
22e4d757e4 | ||
|
|
3b98e8de80 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: Bigcapital # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
node_modules/
|
||||
data
|
||||
|
||||
# Docker volumes data directory
|
||||
/data
|
||||
|
||||
# Production env file
|
||||
.env
|
||||
|
||||
test-results/
|
||||
28
CHANGELOG.md
28
CHANGELOG.md
@@ -2,6 +2,34 @@
|
||||
|
||||
All notable changes to Bigcapital server-side will be in this file.
|
||||
|
||||
# [0.10.1] - 25-09-2023
|
||||
|
||||
* Fix: Running tenants migration on Docker migration container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/242
|
||||
|
||||
# [0.10.0] - 24-09-2023
|
||||
|
||||
* Added: Tax rates service by @abouolia @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* Added: Tax rates tracking with sale invoices by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
|
||||
* fix(webapp): Table headers sticky for all reports. by @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/240
|
||||
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/200
|
||||
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/199
|
||||
* chore(deps): bump mongoose from 5.13.15 to 5.13.20 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/197
|
||||
|
||||
# [0.9.12] - 29-08-2023
|
||||
|
||||
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
|
||||
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
|
||||
* Fix: typo in bill success message without bill number by @KalliopiPliogka in https://github.com/bigcapitalhq/bigcapital/pull/219
|
||||
* Fix: AP/AR aging summary issue by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/229
|
||||
* Fix: shouldn't write GL entries when save transaction as draft. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/221
|
||||
* Fix: Transaction type of credit note and vendor credit are not defined on account transactions by @abouolia in
|
||||
* Fix: date format of filtering transactions by date range by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/231
|
||||
* Fix: change the default from/date date value of reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/230
|
||||
* Fix: typos in words start with `A` letter by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/227
|
||||
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
|
||||
https://github.com/bigcapitalhq/bigcapital/pull/225
|
||||
|
||||
# [0.9.11] - 23-07-2023
|
||||
|
||||
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
|
||||
|
||||
@@ -48,6 +48,12 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
|
||||
|
||||
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.
|
||||
|
||||
# Contact us
|
||||
|
||||
Meet our sales team for any commercial inquiries.
|
||||
|
||||
<a target="_blank" href="https://cal.com/ahmed-bouhuolia-ekk3ph/30min"><img src="https://cal.com/book-with-cal-dark.svg" alt="Book us with Cal.com"></a>
|
||||
|
||||
# Recognition
|
||||
|
||||
<a href="https://news.ycombinator.com/item?id=36118990">
|
||||
|
||||
@@ -15,8 +15,8 @@ services:
|
||||
- ./data/logs/nginx/:/var/log/nginx
|
||||
- ./docker/certbot/certs/:/var/certs
|
||||
ports:
|
||||
- "${PUBLIC_PROXY_PORT:-80}:80"
|
||||
- "${PUBLIC_PROXY_SSL_PORT:-443}:443"
|
||||
- '${PUBLIC_PROXY_PORT:-80}:80'
|
||||
- '${PUBLIC_PROXY_SSL_PORT:-443}:443'
|
||||
tty: true
|
||||
depends_on:
|
||||
- server
|
||||
@@ -71,7 +71,7 @@ services:
|
||||
# Authentication
|
||||
- JWT_SECRET=${JWT_SECRET}
|
||||
|
||||
# MongoDB
|
||||
# MongoDB
|
||||
- MONGODB_DATABASE_URL=mongodb://mongo/bigcapital
|
||||
|
||||
# Application
|
||||
@@ -92,11 +92,14 @@ services:
|
||||
context: ./
|
||||
dockerfile: docker/migration/Dockerfile
|
||||
environment:
|
||||
# Database
|
||||
- DB_HOST=mysql
|
||||
- DB_USER=${DB_USER}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_CHARSET=${DB_CHARSET}
|
||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||
# Tenants databases
|
||||
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
|
||||
depends_on:
|
||||
- mysql
|
||||
|
||||
@@ -136,7 +139,7 @@ services:
|
||||
build:
|
||||
context: ./docker/redis
|
||||
expose:
|
||||
- "6379"
|
||||
- '6379'
|
||||
volumes:
|
||||
- redis:/data
|
||||
|
||||
|
||||
@@ -34,5 +34,7 @@ WORKDIR /app/packages/server
|
||||
|
||||
RUN git clone https://github.com/vishnubob/wait-for-it.git
|
||||
|
||||
# Once we listen the mysql port run the migration task.
|
||||
CMD ["./wait-for-it/wait-for-it.sh", "mysql:3306", "--", "node", "./build/commands.js", "system:migrate:latest"]
|
||||
ADD docker/migration/start.sh /
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
CMD ["/start.sh"]
|
||||
5
docker/migration/start.sh
Normal file
5
docker/migration/start.sh
Normal file
@@ -0,0 +1,5 @@
|
||||
# Migrate the master system database.
|
||||
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js system:migrate:latest
|
||||
|
||||
# Migrate all tenants.
|
||||
./wait-for-it/wait-for-it.sh mysql:3306 -- node ./build/commands.js tenants:migrate:latest
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.9.6",
|
||||
"npmClient": "npm"
|
||||
"version": "independent",
|
||||
"npmClient": "pnpm",
|
||||
"packages": ["packages/*"]
|
||||
}
|
||||
|
||||
5730
package-lock.json
generated
5730
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "bigcapital-monorepo",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"bootstrap": "lerna exec npm install",
|
||||
"bootstrap": "lerna exec pnpm install",
|
||||
"dev": "lerna run dev",
|
||||
"build": "lerna run build",
|
||||
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
|
||||
@@ -13,10 +13,6 @@
|
||||
"test:e2e": "playwright test",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"shared/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.4.2",
|
||||
"@commitlint/config-conventional": "^17.4.2",
|
||||
|
||||
3
packages/server/.gitignore
vendored
3
packages/server/.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
/node_modules/
|
||||
/.env
|
||||
/storage
|
||||
package-lock.json
|
||||
stdout.log
|
||||
/dist
|
||||
/build
|
||||
/build
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bigcapital/server",
|
||||
"version": "0.9.5",
|
||||
"version": "0.10.1",
|
||||
"description": "",
|
||||
"main": "src/server.ts",
|
||||
"scripts": {
|
||||
@@ -35,6 +35,7 @@
|
||||
"babel-loader": "^9.1.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
"country-codes-list": "^1.6.8",
|
||||
"cpy": "^8.1.2",
|
||||
@@ -72,6 +73,8 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"moment": "^2.24.0",
|
||||
"moment-range": "^4.0.2",
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mongodb": "^6.1.0",
|
||||
"mongoose": "^5.10.0",
|
||||
"mustache": "^3.0.3",
|
||||
"mysql": "^2.17.1",
|
||||
@@ -131,7 +134,7 @@
|
||||
"rimraf": "^3.0.2",
|
||||
"rtlcss": "^3.3.0",
|
||||
"run-script-webpack-plugin": "^0.1.1",
|
||||
"sass": "^1.37.5",
|
||||
"sass": "^1.58.0",
|
||||
"sinon": "^7.4.2",
|
||||
"start-server-webpack-plugin": "^2.2.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
|
||||
@@ -20,6 +20,7 @@ import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
|
||||
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
|
||||
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
|
||||
import SalesTaxLiabilitySummary from './FinancialStatements/SalesTaxLiabilitySummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
@@ -68,40 +69,44 @@ export default class FinancialStatementsService {
|
||||
);
|
||||
router.use(
|
||||
'/customer-balance-summary',
|
||||
Container.get(CustomerBalanceSummaryController).router(),
|
||||
Container.get(CustomerBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/vendor-balance-summary',
|
||||
Container.get(VendorBalanceSummaryController).router(),
|
||||
Container.get(VendorBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-customers',
|
||||
Container.get(TransactionsByCustomers).router(),
|
||||
Container.get(TransactionsByCustomers).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
Container.get(TransactionsByVendors).router()
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
Container.get(CashFlowStatementController).router()
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
Container.get(InventoryDetailsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-reference',
|
||||
Container.get(TransactionsByReferenceController).router(),
|
||||
Container.get(TransactionsByReferenceController).router()
|
||||
);
|
||||
router.use(
|
||||
'/cashflow-account-transactions',
|
||||
Container.get(CashflowAccountTransactions).router(),
|
||||
Container.get(CashflowAccountTransactions).router()
|
||||
);
|
||||
router.use(
|
||||
'/project-profitability-summary',
|
||||
Container.get(ProjectProfitabilityController).router(),
|
||||
)
|
||||
Container.get(ProjectProfitabilityController).router()
|
||||
);
|
||||
router.use(
|
||||
'/sales-tax-liability-summary',
|
||||
Container.get(SalesTaxLiabilitySummary).router()
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,13 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
|
||||
return [
|
||||
...this.sheetNumberFormatValidationSchema,
|
||||
query('as_date').optional().isISO8601(),
|
||||
query('aging_days_before').optional().isNumeric().toInt(),
|
||||
query('aging_periods').optional().isNumeric().toInt(),
|
||||
|
||||
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('vendors_ids').optional().isArray({ min: 1 }),
|
||||
query('vendors_ids.*').isInt({ min: 1 }).toInt(),
|
||||
|
||||
query('none_zero').default(true).isBoolean().toBoolean(),
|
||||
|
||||
// Filtering by branches.
|
||||
@@ -53,15 +56,36 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const table = await this.APAgingSummaryService.APAgingSummaryTable(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.rows,
|
||||
columns: table.columns,
|
||||
},
|
||||
meta: table.meta,
|
||||
query: table.query,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
const { data, columns, query, meta } =
|
||||
await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
|
||||
|
||||
query('as_date').optional().isISO8601(),
|
||||
|
||||
query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').optional().isInt({ max: 12 }).toInt(),
|
||||
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(),
|
||||
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
|
||||
|
||||
query('customers_ids').optional().isArray({ min: 1 }),
|
||||
query('customers_ids.*').isInt({ min: 1 }).toInt(),
|
||||
@@ -58,15 +58,36 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const { data, columns, query, meta } =
|
||||
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const table = await this.ARAgingSummaryService.ARAgingSummaryTable(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
table: {
|
||||
rows: table.rows,
|
||||
columns: table.columns,
|
||||
},
|
||||
meta: table.meta,
|
||||
query: table.query,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
const { data, columns, query, meta } =
|
||||
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
|
||||
|
||||
return res.status(200).send({
|
||||
data: this.transfromToResponse(data),
|
||||
columns: this.transfromToResponse(columns),
|
||||
query: this.transfromToResponse(query),
|
||||
meta: this.transfromToResponse(meta),
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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 { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SalesTaxLiabilitySummaryService } from '@/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService';
|
||||
|
||||
export default class SalesTaxLiabilitySummary extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
private salesTaxLiabilitySummaryService: SalesTaxLiabilitySummaryService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.salesTaxLiabilitySummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieves the sales tax liability summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async salesTaxLiabilitySummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const salesTaxLiabilityTable =
|
||||
await this.salesTaxLiabilitySummaryService.salesTaxLiabilitySummaryTable(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
table: salesTaxLiabilityTable.table,
|
||||
query: salesTaxLiabilityTable.query,
|
||||
meta: salesTaxLiabilityTable.meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
const salesTaxLiability =
|
||||
await this.salesTaxLiabilitySummaryService.salesTaxLiability(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: salesTaxLiability.data,
|
||||
query: salesTaxLiability.query,
|
||||
meta: salesTaxLiability.meta,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,8 +169,9 @@ export default class SaleInvoicesController extends BaseController {
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('is_inclusive_tax').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
@@ -183,6 +184,15 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isString(),
|
||||
check('entries.*.tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -756,6 +766,16 @@ export default class SaleInvoicesController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 5000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 5100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
278
packages/server/src/api/controllers/TaxRates/TaxRates.ts
Normal file
278
packages/server/src/api/controllers/TaxRates/TaxRates.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { body, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { TaxRatesApplication } from '@/services/TaxRates/TaxRatesApplication';
|
||||
import CheckAbilities from '@/api/middleware/CheckPolicies';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from '@/services/TaxRates/constants';
|
||||
import { AbilitySubject, TaxRateAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class TaxRatesController extends BaseController {
|
||||
@Inject()
|
||||
private taxRatesApplication: TaxRatesApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckAbilities(TaxRateAction.CREATE, AbilitySubject.TaxRate),
|
||||
this.taxRateValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt(), ...this.taxRateValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/active',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.activateTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactive',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.inactivateTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.DELETE, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTaxRates.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax rate validation schema.
|
||||
*/
|
||||
private get taxRateValidationSchema() {
|
||||
return [
|
||||
body('name').exists(),
|
||||
body('code').exists().isString(),
|
||||
body('rate').exists().isNumeric().toFloat(),
|
||||
body('description').optional().trim().isString(),
|
||||
body('is_non_recoverable').optional().isBoolean().default(false),
|
||||
body('is_compound').optional().isBoolean().default(false),
|
||||
body('active').optional().isBoolean().default(false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async createTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const createTaxRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.createTaxRate(
|
||||
tenantId,
|
||||
createTaxRateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: taxRate,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async editTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const editTaxRateDTO = this.matchedBodyData(req);
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.editTaxRate(
|
||||
tenantId,
|
||||
taxRateId,
|
||||
editTaxRateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: taxRate,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async deleteTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.deleteTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The tax rate has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async getTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.getTaxRate(
|
||||
tenantId,
|
||||
taxRateId
|
||||
);
|
||||
return res.status(200).send({ data: taxRate });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async getTaxRates(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const taxRates = await this.taxRatesApplication.getTaxRates(tenantId);
|
||||
|
||||
return res.status(200).send({ data: taxRates });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
public async inactivateTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.inactivateTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: taxRateId,
|
||||
message: 'The given tax rate has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public async activateTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.activateTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: taxRateId,
|
||||
message: 'The given tax rate has been activated 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) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === ERRORS.TAX_CODE_NOT_UNIQUE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_CODE_NOT_UNIQUE, code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_NOT_FOUND) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_NOT_FOUND, code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_ALREADY_INACTIVE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_ALREADY_INACTIVE, code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_ALREADY_ACTIVE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_ALREADY_ACTIVE, code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import { InventoryItemsCostController } from './controllers/Inventory/Inventorty
|
||||
import { ProjectsController } from './controllers/Projects/Projects';
|
||||
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
||||
import { ProjectTimesController } from './controllers/Projects/Times';
|
||||
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
@@ -129,6 +130,7 @@ export default () => {
|
||||
);
|
||||
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||
|
||||
|
||||
28
packages/server/src/data/TransactionTypes.ts
Executable file
28
packages/server/src/data/TransactionTypes.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
export const TransactionTypes = {
|
||||
SaleInvoice: 'Sale invoice',
|
||||
SaleReceipt: 'Sale receipt',
|
||||
PaymentReceive: 'Payment receive',
|
||||
Bill: 'Bill',
|
||||
BillPayment: 'Payment made',
|
||||
VendorOpeningBalance: 'Vendor opening balance',
|
||||
CustomerOpeningBalance: 'Customer opening balance',
|
||||
InventoryAdjustment: 'Inventory adjustment',
|
||||
ManualJournal: 'Manual journal',
|
||||
Journal: 'Manual journal',
|
||||
Expense: 'Expense',
|
||||
OwnerContribution: 'Owner contribution',
|
||||
TransferToAccount: 'Transfer to account',
|
||||
TransferFromAccount: 'Transfer from account',
|
||||
OtherIncome: 'Other income',
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
InvoiceWriteOff: 'Invoice write-off',
|
||||
|
||||
CreditNote: 'transaction_type.credit_note',
|
||||
VendorCredit: 'transaction_type.vendor_credit',
|
||||
|
||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||
|
||||
LandedCost: 'transaction_type.landed_cost',
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
exports.up = (knex) => {
|
||||
return knex.schema
|
||||
.createTable('tax_rates', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('code');
|
||||
table.decimal('rate');
|
||||
table.string('description');
|
||||
table.boolean('is_non_recoverable').defaultTo(false);
|
||||
table.boolean('is_compound').defaultTo(false);
|
||||
table.boolean('active').defaultTo(false);
|
||||
table.date('deleted_at');
|
||||
table.timestamps();
|
||||
})
|
||||
.table('items_entries', (table) => {
|
||||
table.boolean('is_inclusive_tax').defaultTo(false);
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.decimal('tax_rate').unsigned();
|
||||
})
|
||||
.table('sales_invoices', (table) => {
|
||||
table.boolean('is_inclusive_tax').defaultTo(false);
|
||||
table.decimal('tax_amount_withheld');
|
||||
})
|
||||
.createTable('tax_rate_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
table.decimal('rate').unsigned();
|
||||
table.integer('tax_account_id').unsigned();
|
||||
})
|
||||
.table('accounts_transactions', (table) => {
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.decimal('tax_rate').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {
|
||||
return knex.schema.dropTableIfExists('tax_rates');
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TenantSeeder } from '@/lib/Seeder/TenantSeeder';
|
||||
import { InitialTaxRates } from '../data/TaxRates';
|
||||
|
||||
export default class SeedTaxRates extends TenantSeeder {
|
||||
/**
|
||||
* Seeds initial tax rates to the organization.
|
||||
*/
|
||||
up(knex) {
|
||||
return knex('tax_rates').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('tax_rates').insert(InitialTaxRates);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TenantSeeder } from '@/lib/Seeder/TenantSeeder';
|
||||
import { InitialTaxRates } from '../data/TaxRates';
|
||||
|
||||
export default class UpdateTaxPayableAccount extends TenantSeeder {
|
||||
/**
|
||||
* Seeds initial tax rates to the organization.
|
||||
*/
|
||||
up(knex) {
|
||||
return knex('accounts').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('accounts').where('slug', 'tax-payable').update({
|
||||
account_type: 'tax-payable',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
30
packages/server/src/database/seeds/data/TaxRates.ts
Normal file
30
packages/server/src/database/seeds/data/TaxRates.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const InitialTaxRates = [
|
||||
{
|
||||
name: 'Tax Exempt',
|
||||
code: 'TAX-EXEMPT',
|
||||
description: 'Exempts goods or services from taxes.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Tax on Purchases',
|
||||
code: 'TAX-PURCHASES',
|
||||
description: 'Fee added to the cost when you buy items.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Tax on Sales',
|
||||
code: 'TAX-SALES',
|
||||
description: 'Fee added to the cost when you sell items.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Sales Tax on Imports',
|
||||
code: 'TAX-IMPORTS',
|
||||
description: 'Fee added to the cost when you sale to another country.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,17 @@
|
||||
export const TaxPayableAccount = {
|
||||
name: 'Tax Payable',
|
||||
slug: 'tax-payable',
|
||||
account_type: 'tax-payable',
|
||||
code: '20006',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
name:'Bank Account',
|
||||
name: 'Bank Account',
|
||||
slug: 'bank-account',
|
||||
account_type: 'bank',
|
||||
code: '10001',
|
||||
@@ -11,7 +21,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Saving Bank Account',
|
||||
name: 'Saving Bank Account',
|
||||
slug: 'saving-bank-account',
|
||||
account_type: 'bank',
|
||||
code: '10002',
|
||||
@@ -21,7 +31,7 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Undeposited Funds',
|
||||
name: 'Undeposited Funds',
|
||||
slug: 'undeposited-funds',
|
||||
account_type: 'cash',
|
||||
code: '10003',
|
||||
@@ -31,7 +41,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Petty Cash',
|
||||
name: 'Petty Cash',
|
||||
slug: 'petty-cash',
|
||||
account_type: 'cash',
|
||||
code: '10004',
|
||||
@@ -41,7 +51,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Computer Equipment',
|
||||
name: 'Computer Equipment',
|
||||
slug: 'computer-equipment',
|
||||
code: '10005',
|
||||
account_type: 'fixed-asset',
|
||||
@@ -52,7 +62,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Office Equipment',
|
||||
name: 'Office Equipment',
|
||||
slug: 'office-equipment',
|
||||
code: '10006',
|
||||
account_type: 'fixed-asset',
|
||||
@@ -63,7 +73,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Accounts Receivable (A/R)',
|
||||
name: 'Accounts Receivable (A/R)',
|
||||
slug: 'accounts-receivable',
|
||||
account_type: 'accounts-receivable',
|
||||
code: '10007',
|
||||
@@ -73,7 +83,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Inventory Asset',
|
||||
name: 'Inventory Asset',
|
||||
slug: 'inventory-asset',
|
||||
code: '10008',
|
||||
account_type: 'inventory',
|
||||
@@ -81,12 +91,13 @@ export default [
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description:'An account that holds valuation of products or goods that available for sale.',
|
||||
description:
|
||||
'An account that holds valuation of products or goods that available for sale.',
|
||||
},
|
||||
|
||||
// Libilities
|
||||
{
|
||||
name:'Accounts Payable (A/P)',
|
||||
name: 'Accounts Payable (A/P)',
|
||||
slug: 'accounts-payable',
|
||||
account_type: 'accounts-payable',
|
||||
parent_account_id: null,
|
||||
@@ -97,38 +108,39 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Owner A Drawings',
|
||||
name: 'Owner A Drawings',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
code: '20002',
|
||||
description:'Withdrawals by the owners.',
|
||||
description: 'Withdrawals by the owners.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Loan',
|
||||
name: 'Loan',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20003',
|
||||
description:'Money that has been borrowed from a creditor.',
|
||||
description: 'Money that has been borrowed from a creditor.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Opening Balance Liabilities',
|
||||
name: 'Opening Balance Liabilities',
|
||||
slug: 'opening-balance-liabilities',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20004',
|
||||
description:'This account will hold the difference in the debits and credits entered during the opening balance..',
|
||||
description:
|
||||
'This account will hold the difference in the debits and credits entered during the opening balance..',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Revenue Received in Advance',
|
||||
name: 'Revenue Received in Advance',
|
||||
slug: 'revenue-received-in-advance',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
@@ -138,34 +150,27 @@ export default [
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Sales Tax Payable',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20006',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
TaxPayableAccount,
|
||||
|
||||
// Equity
|
||||
{
|
||||
name:'Retained Earnings',
|
||||
name: 'Retained Earnings',
|
||||
slug: 'retained-earnings',
|
||||
account_type: 'equity',
|
||||
code: '30001',
|
||||
description:'Retained earnings tracks net income from previous fiscal years.',
|
||||
description:
|
||||
'Retained earnings tracks net income from previous fiscal years.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Opening Balance Equity',
|
||||
name: 'Opening Balance Equity',
|
||||
slug: 'opening-balance-equity',
|
||||
account_type: 'equity',
|
||||
code: '30002',
|
||||
description:'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
|
||||
description:
|
||||
'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
@@ -181,11 +186,12 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:`Drawings`,
|
||||
name: `Drawings`,
|
||||
slug: 'drawings',
|
||||
account_type: 'equity',
|
||||
code: '30003',
|
||||
description:'Goods purchased with the intention of selling these to customers',
|
||||
description:
|
||||
'Goods purchased with the intention of selling these to customers',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
@@ -193,7 +199,7 @@ export default [
|
||||
|
||||
// Expenses
|
||||
{
|
||||
name:'Other Expenses',
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
@@ -204,18 +210,18 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Cost of Goods Sold',
|
||||
name: 'Cost of Goods Sold',
|
||||
slug: 'cost-of-goods-sold',
|
||||
account_type: 'cost-of-goods-sold',
|
||||
parent_account_id: null,
|
||||
code: '40002',
|
||||
description:'Tracks the direct cost of the goods sold.',
|
||||
description: 'Tracks the direct cost of the goods sold.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Office expenses',
|
||||
name: 'Office expenses',
|
||||
slug: 'office-expenses',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -226,7 +232,7 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Rent',
|
||||
name: 'Rent',
|
||||
slug: 'rent',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -237,29 +243,30 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Exchange Gain or Loss',
|
||||
name: 'Exchange Gain or Loss',
|
||||
slug: 'exchange-grain-loss',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
code: '40005',
|
||||
description:'Tracks the gain and losses of the exchange differences.',
|
||||
description: 'Tracks the gain and losses of the exchange differences.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Bank Fees and Charges',
|
||||
name: 'Bank Fees and Charges',
|
||||
slug: 'bank-fees-and-charges',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40006',
|
||||
description: '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.',
|
||||
description:
|
||||
'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.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Depreciation Expense',
|
||||
name: 'Depreciation Expense',
|
||||
slug: 'depreciation-expense',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -272,7 +279,7 @@ export default [
|
||||
|
||||
// Income
|
||||
{
|
||||
name:'Sales of Product Income',
|
||||
name: 'Sales of Product Income',
|
||||
slug: 'sales-of-product-income',
|
||||
account_type: 'income',
|
||||
predefined: 1,
|
||||
@@ -283,7 +290,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Sales of Service Income',
|
||||
name: 'Sales of Service Income',
|
||||
slug: 'sales-of-service-income',
|
||||
account_type: 'income',
|
||||
predefined: 0,
|
||||
@@ -294,7 +301,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Uncategorized Income',
|
||||
name: 'Uncategorized Income',
|
||||
slug: 'uncategorized-income',
|
||||
account_type: 'income',
|
||||
parent_account_id: null,
|
||||
@@ -305,14 +312,15 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Other Income',
|
||||
name: 'Other Income',
|
||||
slug: 'other-income',
|
||||
account_type: 'other-income',
|
||||
parent_account_id: null,
|
||||
code: '50004',
|
||||
description:'The income activities are not associated to the core business.',
|
||||
description:
|
||||
'The income activities are not associated to the core business.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
}
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,51 +1,36 @@
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingPeriodTotal,
|
||||
IAgingAmount
|
||||
IAgingAmount,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
} from './AgingReport';
|
||||
import {
|
||||
INumberFormatQuery
|
||||
} from './FinancialStatements';
|
||||
import { INumberFormatQuery } from './FinancialStatements';
|
||||
|
||||
export interface IAPAgingSummaryQuery {
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: INumberFormatQuery;
|
||||
export interface IAPAgingSummaryQuery extends IAgingSummaryQuery {
|
||||
vendorsIds: number[];
|
||||
noneZero: boolean;
|
||||
|
||||
branchesIds?: number[]
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryVendor {
|
||||
vendorName: string,
|
||||
current: IAgingAmount,
|
||||
aging: IAgingPeriodTotal[],
|
||||
total: IAgingAmount,
|
||||
};
|
||||
export interface IAPAgingSummaryVendor extends IAgingSummaryContact {
|
||||
vendorName: string;
|
||||
}
|
||||
|
||||
export interface IAPAgingSummaryTotal {
|
||||
current: IAgingAmount,
|
||||
aging: IAgingPeriodTotal[],
|
||||
total: IAgingAmount,
|
||||
};
|
||||
export interface IAPAgingSummaryTotal extends IAgingSummaryTotal {}
|
||||
|
||||
export interface IAPAgingSummaryData {
|
||||
vendors: IAPAgingSummaryVendor[],
|
||||
total: IAPAgingSummaryTotal,
|
||||
};
|
||||
export interface IAPAgingSummaryData extends IAgingSummaryData {
|
||||
vendors: IAPAgingSummaryVendor[];
|
||||
}
|
||||
|
||||
export type IAPAgingSummaryColumns = IAgingPeriod[];
|
||||
|
||||
|
||||
export interface IARAgingSummaryMeta {
|
||||
baseCurrency: string,
|
||||
organizationName: string,
|
||||
baseCurrency: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IAPAgingSummaryMeta {
|
||||
baseCurrency: string,
|
||||
organizationName: string,
|
||||
}
|
||||
baseCurrency: string;
|
||||
organizationName: string;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
import { IAgingPeriod, IAgingPeriodTotal, IAgingAmount } from './AgingReport';
|
||||
import { INumberFormatQuery } from './FinancialStatements';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
} from './AgingReport';
|
||||
|
||||
export interface IARAgingSummaryQuery {
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: INumberFormatQuery;
|
||||
export interface IARAgingSummaryQuery extends IAgingSummaryQuery {
|
||||
customersIds: number[];
|
||||
branchesIds: number[];
|
||||
noneZero: boolean;
|
||||
}
|
||||
|
||||
export interface IARAgingSummaryCustomer {
|
||||
export interface IARAgingSummaryCustomer extends IAgingSummaryContact {
|
||||
customerName: string;
|
||||
current: IAgingAmount;
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IARAgingSummaryTotal {
|
||||
current: IAgingAmount;
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
export interface IARAgingSummaryTotal extends IAgingSummaryTotal {}
|
||||
|
||||
export interface IARAgingSummaryData {
|
||||
export interface IARAgingSummaryData extends IAgingSummaryData {
|
||||
customers: IARAgingSummaryCustomer[];
|
||||
total: IARAgingSummaryTotal;
|
||||
}
|
||||
|
||||
export type IARAgingSummaryColumns = IAgingPeriod[];
|
||||
|
||||
export interface IARAgingSummaryMeta {
|
||||
organizationName: string,
|
||||
baseCurrency: string,
|
||||
}
|
||||
organizationName: string;
|
||||
baseCurrency: string;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ export interface IAccountTransaction {
|
||||
projectId?: number;
|
||||
|
||||
account?: IAccount;
|
||||
|
||||
taxRateId?: number;
|
||||
taxRate?: number;
|
||||
}
|
||||
export interface IAccountResponse extends IAccount {}
|
||||
|
||||
@@ -150,3 +153,11 @@ export enum AccountAction {
|
||||
VIEW = 'View',
|
||||
TransactionsLocking = 'TransactionsLocking',
|
||||
}
|
||||
|
||||
|
||||
export enum TaxRateAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
|
||||
import { INumberFormatQuery } from './FinancialStatements';
|
||||
|
||||
export interface IAgingPeriodTotal extends IAgingPeriod {
|
||||
total: IAgingAmount;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IAgingAmount {
|
||||
amount: number;
|
||||
@@ -20,3 +23,22 @@ export interface IAgingSummaryContact {
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryQuery {
|
||||
asDate: Date | string;
|
||||
agingDaysBefore: number;
|
||||
agingPeriods: number;
|
||||
numberFormat: INumberFormatQuery;
|
||||
branchesIds: number[];
|
||||
noneZero: boolean;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryTotal {
|
||||
current: IAgingAmount;
|
||||
aging: IAgingPeriodTotal[];
|
||||
total: IAgingAmount;
|
||||
}
|
||||
|
||||
export interface IAgingSummaryData {
|
||||
total: IAgingSummaryTotal;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export enum ReportsAction {
|
||||
READ_INVENTORY_ITEM_DETAILS = 'read-inventory-item-details',
|
||||
READ_CASHFLOW_ACCOUNT_TRANSACTION = 'read-cashflow-account-transactions',
|
||||
READ_PROJECT_PROFITABILITY_SUMMARY = 'read-project-profitability-summary',
|
||||
READ_SALES_TAX_LIABILITY_SUMMARY = 'read-sales-tax-liability-summary',
|
||||
}
|
||||
|
||||
export interface IFinancialSheetBranchesQuery {
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface IItemEntry {
|
||||
rate: number;
|
||||
amount: number;
|
||||
|
||||
total: number;
|
||||
amountInclusingTax: number;
|
||||
amountExludingTax: number;
|
||||
discountAmount: number;
|
||||
|
||||
landedCost: number;
|
||||
allocatedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
@@ -32,6 +37,10 @@ export interface IItemEntry {
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxRateId: number | null;
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
|
||||
item?: IItem;
|
||||
|
||||
allocatedCostEntries?: IBillLandedCostEntry[];
|
||||
@@ -46,6 +55,9 @@ export interface IItemEntryDTO {
|
||||
projectRefId?: number;
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxRateId?: number;
|
||||
taxCode?: string;
|
||||
}
|
||||
|
||||
export enum ProjectLinkRefType {
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface ILedgerEntry {
|
||||
branchId?: number;
|
||||
projectId?: number;
|
||||
|
||||
taxRateId?: number;
|
||||
taxRate?: number;
|
||||
|
||||
entryId?: number;
|
||||
createdAt?: Date;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IItemEntry } from './ItemEntry';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
|
||||
export interface ISaleEstimate {
|
||||
@@ -29,7 +29,7 @@ export interface ISaleEstimateDTO {
|
||||
estimateDate?: Date;
|
||||
reference?: string;
|
||||
estimateNumber?: string;
|
||||
entries: IItemEntry[];
|
||||
entries: IItemEntryDTO[];
|
||||
note: string;
|
||||
termsConditions: string;
|
||||
sendToEmail: string;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ISystemUser, IAccount } from '@/interfaces';
|
||||
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
|
||||
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
|
||||
export interface ISaleInvoice {
|
||||
id: number;
|
||||
balance: number;
|
||||
amount: number;
|
||||
amountLocal?: number;
|
||||
paymentAmount: number;
|
||||
currencyCode: string;
|
||||
exchangeRate?: number;
|
||||
@@ -27,12 +28,21 @@ export interface ISaleInvoice {
|
||||
branchId?: number;
|
||||
projectId?: number;
|
||||
|
||||
localAmount?: number;
|
||||
|
||||
localWrittenoffAmount?: number;
|
||||
writtenoffAmount?: number;
|
||||
writtenoffAmountLocal?: number;
|
||||
writtenoffExpenseAccountId?: number;
|
||||
|
||||
writtenoffExpenseAccount?: IAccount;
|
||||
|
||||
taxAmountWithheld: number;
|
||||
taxAmountWithheldLocal: number;
|
||||
taxes: ITaxTransaction[];
|
||||
|
||||
total: number;
|
||||
totalLocal: number;
|
||||
|
||||
subtotal: number;
|
||||
subtotalLocal: number;
|
||||
subtotalExludingTax: number;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
@@ -44,12 +54,15 @@ export interface ISaleInvoiceDTO {
|
||||
exchangeRate?: number;
|
||||
invoiceMessage: string;
|
||||
termsConditions: string;
|
||||
isTaxExclusive: boolean;
|
||||
entries: IItemEntryDTO[];
|
||||
delivered: boolean;
|
||||
|
||||
warehouseId?: number | null;
|
||||
projectId?: number;
|
||||
branchId?: number | null;
|
||||
|
||||
isInclusiveTax?: boolean;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
|
||||
|
||||
51
packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
Normal file
51
packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface SalesTaxLiabilitySummaryQuery {
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
basis: 'cash' | 'accrual';
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryAmount {
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryTotal {
|
||||
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryRate {
|
||||
id: number;
|
||||
taxName: string;
|
||||
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxPercentage: any;
|
||||
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
}
|
||||
|
||||
export enum SalesTaxLiabilitySummaryTableRowType {
|
||||
TaxRate = 'TaxRate',
|
||||
Total = 'Total',
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryReportData {
|
||||
taxRates: SalesTaxLiabilitySummaryRate[];
|
||||
total: SalesTaxLiabilitySummaryTotal;
|
||||
}
|
||||
|
||||
export type SalesTaxLiabilitySummaryPayableById = Record<
|
||||
string,
|
||||
{ taxRateId: number; credit: number; debit: number }
|
||||
>;
|
||||
|
||||
export type SalesTaxLiabilitySummarySalesById = Record<
|
||||
string,
|
||||
{ taxRateId: number; credit: number; debit: number }
|
||||
>;
|
||||
|
||||
export interface SalesTaxLiabilitySummaryMeta {
|
||||
organizationName: string;
|
||||
baseCurrency: string;
|
||||
}
|
||||
88
packages/server/src/interfaces/TaxRate.ts
Normal file
88
packages/server/src/interfaces/TaxRate.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export interface ITaxRate {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
rate: number;
|
||||
description: string;
|
||||
IsNonRecoverable: boolean;
|
||||
IsCompound: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ICommonTaxRateDTO {
|
||||
name: string;
|
||||
code: string;
|
||||
rate: number;
|
||||
description: string;
|
||||
IsNonRecoverable: boolean;
|
||||
IsCompound: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
export interface ICreateTaxRateDTO extends ICommonTaxRateDTO {}
|
||||
export interface IEditTaxRateDTO extends ICommonTaxRateDTO {}
|
||||
|
||||
export interface ITaxRateCreatingPayload {
|
||||
createTaxRateDTO: ICreateTaxRateDTO;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateCreatedPayload {
|
||||
createTaxRateDTO: ICreateTaxRateDTO;
|
||||
taxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateEditingPayload {
|
||||
editTaxRateDTO: IEditTaxRateDTO;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateEditedPayload {
|
||||
editTaxRateDTO: IEditTaxRateDTO;
|
||||
oldTaxRate: ITaxRate;
|
||||
taxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateDeletingPayload {
|
||||
oldTaxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateActivatingPayload {
|
||||
taxRateId: number;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateActivatedPayload {
|
||||
taxRateId: number;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateDeletedPayload {
|
||||
oldTaxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxTransaction {
|
||||
id?: number;
|
||||
taxRateId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
rate: number;
|
||||
taxAccountId: number;
|
||||
}
|
||||
|
||||
export enum TaxRateAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export * from './Project';
|
||||
export * from './Tasks';
|
||||
export * from './Times';
|
||||
export * from './ProjectProfitabilitySummary';
|
||||
export * from './TaxRate';
|
||||
|
||||
export interface I18nService {
|
||||
__: (input: string) => string;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber } from 'utils';
|
||||
import { isArrayLikeObject } from 'lodash/fp';
|
||||
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -82,6 +81,7 @@ export class Transformer {
|
||||
const normlizedItem = this.normalizeModelItem(item);
|
||||
|
||||
return R.compose(
|
||||
sortObjectKeysAlphabetically,
|
||||
this.transform,
|
||||
R.when(this.hasExcludeAttributes, this.excludeAttributesTransformed),
|
||||
this.includeAttributesTransformed
|
||||
|
||||
@@ -79,6 +79,8 @@ import { ProjectBillableTasksSubscriber } from '@/services/Projects/Projects/Pro
|
||||
import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ProjectBillableExpenseSubscriber';
|
||||
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
|
||||
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
|
||||
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
|
||||
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -185,5 +187,9 @@ export const susbcribers = () => {
|
||||
ProjectBillableTasksSubscriber,
|
||||
ProjectBillableExpensesSubscriber,
|
||||
ProjectBillableBillSubscriber,
|
||||
|
||||
// Tax Rates
|
||||
SaleInvoiceTaxRateValidateSubscriber,
|
||||
WriteInvoiceTaxTransactionsSubscriber,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -58,6 +58,8 @@ import ItemWarehouseQuantity from 'models/ItemWarehouseQuantity';
|
||||
import Project from 'models/Project';
|
||||
import Time from 'models/Time';
|
||||
import Task from 'models/Task';
|
||||
import TaxRate from 'models/TaxRate';
|
||||
import TaxRateTransaction from 'models/TaxRateTransaction';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
@@ -119,6 +121,8 @@ export default (knex) => {
|
||||
Project,
|
||||
Time,
|
||||
Task,
|
||||
TaxRate,
|
||||
TaxRateTransaction,
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
};
|
||||
|
||||
@@ -6,6 +6,10 @@ import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
|
||||
export default class AccountTransaction extends TenantModel {
|
||||
referenceType: string;
|
||||
credit: number;
|
||||
debit: number;
|
||||
exchangeRate: number;
|
||||
taxRate: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
@@ -25,7 +29,23 @@ export default class AccountTransaction extends TenantModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['referenceTypeFormatted'];
|
||||
return ['referenceTypeFormatted', 'creditLocal', 'debitLocal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get creditLocal() {
|
||||
return this.credit * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the debit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get debitLocal() {
|
||||
return this.debit * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -59,15 +79,9 @@ export default class AccountTransaction extends TenantModel {
|
||||
}
|
||||
},
|
||||
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const fromDate = moment(startDate)
|
||||
.utcOffset(0)
|
||||
.startOf(type)
|
||||
.format(dateFormat);
|
||||
const toDate = moment(endDate)
|
||||
.utcOffset(0)
|
||||
.endOf(type)
|
||||
.format(dateFormat);
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
if (startDate) {
|
||||
query.where('date', '>=', fromDate);
|
||||
@@ -111,7 +125,6 @@ export default class AccountTransaction extends TenantModel {
|
||||
query.modify('filterDateRange', null, toDate);
|
||||
query.modify('sumationCreditDebit');
|
||||
},
|
||||
|
||||
contactsOpeningBalance(
|
||||
query,
|
||||
openingDate,
|
||||
|
||||
@@ -33,7 +33,7 @@ export default class InventoryCostLotTracker extends TenantModel {
|
||||
query.groupBy('item_id');
|
||||
},
|
||||
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class InventoryTransaction extends TenantModel {
|
||||
static get modifiers() {
|
||||
return {
|
||||
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
|
||||
|
||||
export default class ItemEntry extends TenantModel {
|
||||
public taxRate: number;
|
||||
public discount: number;
|
||||
public quantity: number;
|
||||
public rate: number;
|
||||
public isInclusiveTax: number;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
* @returns {string}
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'items_entries';
|
||||
@@ -11,26 +19,89 @@ export default class ItemEntry extends TenantModel {
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['amount'];
|
||||
return [
|
||||
'amount',
|
||||
'taxAmount',
|
||||
'amountExludingTax',
|
||||
'amountInclusingTax',
|
||||
'total',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry total.
|
||||
* Amount of item entry includes tax and subtracted discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.amountInclusingTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount.
|
||||
* Amount of item entry that may include or exclude tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amount() {
|
||||
return ItemEntry.calcAmount(this);
|
||||
return this.quantity * this.rate;
|
||||
}
|
||||
|
||||
static calcAmount(itemEntry) {
|
||||
const { discount, quantity, rate } = itemEntry;
|
||||
const total = quantity * rate;
|
||||
|
||||
return discount ? total - total * discount * 0.01 : total;
|
||||
/**
|
||||
* Item entry amount including tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountInclusingTax() {
|
||||
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount excluding tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountExludingTax() {
|
||||
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.amount * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag rate fraction.
|
||||
* @returns {number}
|
||||
*/
|
||||
get tagRateFraction() {
|
||||
return this.taxRate / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax amount withheld.
|
||||
* @returns {number}
|
||||
*/
|
||||
get taxAmount() {
|
||||
return this.isInclusiveTax
|
||||
? getInclusiveTaxAmount(this.amount, this.taxRate)
|
||||
: getExlusiveTaxAmount(this.amount, this.taxRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry relations.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Item = require('models/Item');
|
||||
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||
@@ -40,6 +111,7 @@ export default class ItemEntry extends TenantModel {
|
||||
const SaleEstimate = require('models/SaleEstimate');
|
||||
const ProjectTask = require('models/Task');
|
||||
const Expense = require('models/Expense');
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
item: {
|
||||
@@ -86,6 +158,9 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale receipt reference.
|
||||
*/
|
||||
receipt: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: SaleReceipt.default,
|
||||
@@ -96,7 +171,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project task reference.
|
||||
*/
|
||||
projectTaskRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -108,7 +183,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project expense reference.
|
||||
*/
|
||||
projectExpenseRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -120,7 +195,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project bill reference.
|
||||
*/
|
||||
projectBillRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -130,6 +205,18 @@ export default class ItemEntry extends TenantModel {
|
||||
to: 'bills.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Tax rate reference.
|
||||
*/
|
||||
tax: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'items_entries.taxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import { castArray, takeWhile } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
@@ -13,6 +13,17 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public taxAmountWithheld: number;
|
||||
public balance: number;
|
||||
public paymentAmount: number;
|
||||
public exchangeRate: number;
|
||||
public writtenoffAmount: number;
|
||||
public creditedAmount: number;
|
||||
public isInclusiveTax: boolean;
|
||||
public writtenoffAt: Date;
|
||||
public dueDate: Date;
|
||||
public deliveredAt: Date;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -27,6 +38,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get pluralName() {
|
||||
return 'asdfsdf';
|
||||
}
|
||||
@@ -36,35 +50,97 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'dueAmount',
|
||||
'balanceAmount',
|
||||
'isDelivered',
|
||||
'isOverdue',
|
||||
'isPartiallyPaid',
|
||||
'isFullyPaid',
|
||||
'isPaid',
|
||||
'isWrittenoff',
|
||||
'isPaid',
|
||||
|
||||
'dueAmount',
|
||||
'balanceAmount',
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
'filterByBranches',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
'subtotalExludingTax',
|
||||
|
||||
'taxAmountWithheldLocal',
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'writtenoffAmountLocal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice amount in local currency.
|
||||
* Invoice amount.
|
||||
* @todo Sugger attribute to balance, we need to rename the balance to amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAmount() {
|
||||
return this.balance * this.exchangeRate;
|
||||
get amount() {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice local written-off amount.
|
||||
* Invoice amount in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localWrittenoffAmount() {
|
||||
return this.writtenoffAmount * this.exchangeRate;
|
||||
get amountLocal() {
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.amountLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale invoice amount excluding tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalExludingTax() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal - this.taxAmountWithheld
|
||||
: this.subtotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax amount withheld in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get taxAmountWithheldLocal() {
|
||||
return this.taxAmountWithheld * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal
|
||||
: this.subtotal + this.taxAmountWithheld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total in local currency. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +173,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
* @return {boolean}
|
||||
*/
|
||||
get dueAmount() {
|
||||
return Math.max(this.balance - this.balanceAmount, 0);
|
||||
return Math.max(this.total - this.balanceAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +181,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isPartiallyPaid() {
|
||||
return this.dueAmount !== this.balance && this.dueAmount > 0;
|
||||
return this.dueAmount !== this.total && this.dueAmount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,7 +252,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
* Filters the invoices between the given date range.
|
||||
*/
|
||||
filterDateRange(query, startDate, endDate, type = 'day') {
|
||||
const dateFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
const dateFormat = 'YYYY-MM-DD';
|
||||
const fromDate = moment(startDate).startOf(type).format(dateFormat);
|
||||
const toDate = moment(endDate).endOf(type).format(dateFormat);
|
||||
|
||||
@@ -333,6 +409,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
|
||||
const Branch = require('models/Branch');
|
||||
const Account = require('models/Account');
|
||||
const TaxRateTransaction = require('models/TaxRateTransaction');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -382,7 +459,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Invoice may has associated cost transactions.
|
||||
*/
|
||||
costTransactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -397,7 +474,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Invoice may has associated payment entries.
|
||||
*/
|
||||
paymentEntries: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -420,6 +497,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated written-off expense account.
|
||||
*/
|
||||
writtenoffExpenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account.default,
|
||||
@@ -428,6 +508,21 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated tax rate transactions.
|
||||
*/
|
||||
taxes: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: TaxRateTransaction.default,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'tax_rate_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
48
packages/server/src/models/TaxRate.ts
Normal file
48
packages/server/src/models/TaxRate.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||
|
||||
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'tax_rates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete query builder.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return SoftDeleteQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
56
packages/server/src/models/TaxRateTransaction.ts
Normal file
56
packages/server/src/models/TaxRateTransaction.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
|
||||
export default class TaxRateTransaction extends mixin(TenantModel, [
|
||||
ModelSearchable,
|
||||
]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'tax_rate_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Belongs to the tax rate.
|
||||
*/
|
||||
taxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'tax_rate_transactions.taxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Account } from 'models';
|
||||
import TenantRepository from '@/repositories/TenantRepository';
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
|
||||
|
||||
export default class AccountRepository extends TenantRepository {
|
||||
/**
|
||||
@@ -116,7 +117,7 @@ export default class AccountRepository extends TenantRepository {
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
name: this.i18n.__('account.accounts_receivable.currency', {
|
||||
currency: currencyCode
|
||||
currency: currencyCode,
|
||||
}),
|
||||
accountType: 'accounts-receivable',
|
||||
currencyCode,
|
||||
@@ -127,6 +128,29 @@ export default class AccountRepository extends TenantRepository {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find or create tax payable account.
|
||||
* @param {Record<string, string>}extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
async findOrCreateTaxPayable(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: TaxPayableAccount.slug, ...extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...TaxPayableAccount,
|
||||
...extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
findOrCreateAccountsPayable = async (
|
||||
currencyCode: string = '',
|
||||
extraAttrs = {},
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { castArray, sumBy, toArray } from 'lodash';
|
||||
import { IBill, ISystemUser, IAccount } from '@/interfaces';
|
||||
import { castArray } from 'lodash';
|
||||
import JournalPoster from './JournalPoster';
|
||||
import JournalEntry from './JournalEntry';
|
||||
import { IExpense, IExpenseCategory } from '@/interfaces';
|
||||
import { increment } from 'utils';
|
||||
|
||||
export default class JournalCommands {
|
||||
journal: JournalPoster;
|
||||
models: any;
|
||||
@@ -16,7 +12,6 @@ export default class JournalCommands {
|
||||
*/
|
||||
constructor(journal: JournalPoster) {
|
||||
this.journal = journal;
|
||||
|
||||
this.repositories = this.journal.repositories;
|
||||
this.models = this.journal.models;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,9 @@ export default class Ledger implements ILedger {
|
||||
entryId: entry.id,
|
||||
branchId: entry.branchId,
|
||||
projectId: entry.projectId,
|
||||
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,5 +32,8 @@ export const transformLedgerEntryToTransaction = (
|
||||
projectId: entry.projectId,
|
||||
|
||||
costable: entry.costable,
|
||||
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { includes, difference, camelCase, upperFirst } from 'lodash';
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import { IAccount, ICashflowTransactionLine } from '@/interfaces';
|
||||
import { Service } from 'typedi';
|
||||
import { includes, camelCase, upperFirst } from 'lodash';
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { getCashflowTransactionType } from './utils';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class CommandCashflowValidator {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validates the lines accounts type should be cash or bank account.
|
||||
* @param {IAccount} accounts -
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { difference, includes } from 'lodash';
|
||||
import { ICashflowTransactionLine } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { CASHFLOW_TRANSACTION_TYPE, ERRORS } from './constants';
|
||||
import { IAccount } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export default class CommandCashflowTransaction {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import APAgingSummarySheet from './APAgingSummarySheet';
|
||||
import { Tenant } from '@/system/models';
|
||||
import { isEmpty } from 'lodash';
|
||||
import APAgingSummaryTable from './APAgingSummaryTable';
|
||||
|
||||
@Service()
|
||||
export default class PayableAgingSummaryService {
|
||||
@@ -84,7 +85,7 @@ export default class PayableAgingSummaryService {
|
||||
|
||||
// Common query.
|
||||
const commonQuery = (query) => {
|
||||
if (isEmpty(filter.branchesIds)) {
|
||||
if (!isEmpty(filter.branchesIds)) {
|
||||
query.modify('filterByBranches', filter.branchesIds);
|
||||
}
|
||||
};
|
||||
@@ -118,4 +119,21 @@ export default class PayableAgingSummaryService {
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves A/P aging summary in table format.
|
||||
* @param {number} tenantId
|
||||
* @param {IAPAgingSummaryQuery} query
|
||||
*/
|
||||
async APAgingSummaryTable(tenantId: number, query: IAPAgingSummaryQuery) {
|
||||
const report = await this.APAgingSummary(tenantId, query);
|
||||
const table = new APAgingSummaryTable(report.data, query, {});
|
||||
|
||||
return {
|
||||
columns: table.tableColumns(),
|
||||
rows: table.tableRows(),
|
||||
meta: report.meta,
|
||||
query: report.query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ export default class APAgingSummarySheet extends AgingSummaryReport {
|
||||
|
||||
return {
|
||||
vendorName: vendor.displayName,
|
||||
current: this.formatTotalAmount(currentTotal),
|
||||
current: this.formatAmount(currentTotal),
|
||||
aging: agingPeriods,
|
||||
total: this.formatTotalAmount(amount),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IAPAgingSummaryData,
|
||||
IAgingSummaryQuery,
|
||||
ITableColumn,
|
||||
ITableColumnAccessor,
|
||||
ITableRow,
|
||||
} from '@/interfaces';
|
||||
import AgingSummaryTable from './AgingSummaryTable';
|
||||
|
||||
export default class APAgingSummaryTable extends AgingSummaryTable {
|
||||
readonly report: IAPAgingSummaryData;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data
|
||||
* @param {IAgingSummaryQuery} query
|
||||
* @param {any} i18n
|
||||
*/
|
||||
constructor(data: IAPAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
|
||||
super(data, query, i18n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
get contactsRows(): ITableRow[] {
|
||||
return this.contactsNodes(this.report.vendors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'vendor_name', accessor: 'vendorName' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
contactNameTableColumn = (): ITableColumn => {
|
||||
return { label: 'Vendor name', key: 'vendor_name' };
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { IARAgingSummaryQuery, IARAgingSummaryMeta } from '@/interfaces';
|
||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ARAgingSummarySheet from './ARAgingSummarySheet';
|
||||
import { Tenant } from '@/system/models';
|
||||
import ARAgingSummaryTable from './ARAgingSummaryTable';
|
||||
|
||||
@Service()
|
||||
export default class ARAgingSummaryService {
|
||||
@@ -89,12 +90,12 @@ export default class ARAgingSummaryService {
|
||||
};
|
||||
// Retrieve all overdue sale invoices.
|
||||
const overdueSaleInvoices = await SaleInvoice.query()
|
||||
.modify('dueInvoicesFromDate', filter.asDate)
|
||||
.modify('overdueInvoicesFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// Retrieve all due sale invoices.
|
||||
const currentInvoices = await SaleInvoice.query()
|
||||
.modify('overdueInvoicesFromDate', filter.asDate)
|
||||
.modify('dueInvoicesFromDate', filter.asDate)
|
||||
.onBuild(commonQuery);
|
||||
|
||||
// AR aging summary report instance.
|
||||
@@ -117,4 +118,21 @@ export default class ARAgingSummaryService {
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves A/R aging summary in table format.
|
||||
* @param {number} tenantId
|
||||
* @param {IARAgingSummaryQuery} query
|
||||
*/
|
||||
async ARAgingSummaryTable(tenantId: number, query: IARAgingSummaryQuery) {
|
||||
const report = await this.ARAgingSummary(tenantId, query);
|
||||
const table = new ARAgingSummaryTable(report.data, query, {});
|
||||
|
||||
return {
|
||||
columns: table.tableColumns(),
|
||||
rows: table.tableRows(),
|
||||
meta: report.meta,
|
||||
query,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { groupBy, isEmpty, sum } from 'lodash';
|
||||
import { Dictionary, groupBy, isEmpty, sum } from 'lodash';
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
ICustomer,
|
||||
@@ -54,7 +54,6 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
currentSaleInvoices,
|
||||
'customerId'
|
||||
);
|
||||
|
||||
// Initializes the aging periods.
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
@@ -189,7 +188,7 @@ export default class ARAgingSummarySheet extends AgingSummaryReport {
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve AR aging summary report columns.
|
||||
* Retrieve A/R aging summary report columns.
|
||||
* @return {IARAgingSummaryColumns}
|
||||
*/
|
||||
public reportColumns(): IARAgingSummaryColumns {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
IARAgingSummaryData,
|
||||
IAgingSummaryData,
|
||||
IAgingSummaryQuery,
|
||||
ITableColumnAccessor,
|
||||
ITableRow,
|
||||
} from '@/interfaces';
|
||||
import AgingSummaryTable from './AgingSummaryTable';
|
||||
|
||||
export default class ARAgingSummaryTable extends AgingSummaryTable {
|
||||
readonly report: IARAgingSummaryData;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data
|
||||
* @param {IAgingSummaryQuery} query
|
||||
* @param {any} i18n
|
||||
*/
|
||||
constructor(data: IARAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
|
||||
super(data, query, i18n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
get contactsRows(): ITableRow[] {
|
||||
return this.contactsNodes(this.report.customers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'customer_name', accessor: 'customerName' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
IAgingPeriod,
|
||||
IAgingSummaryContact,
|
||||
IAgingSummaryData,
|
||||
IAgingSummaryQuery,
|
||||
IAgingSummaryTotal,
|
||||
ITableColumn,
|
||||
ITableColumnAccessor,
|
||||
ITableRow,
|
||||
} from '@/interfaces';
|
||||
import { tableRowMapper } from '@/utils';
|
||||
import AgingReport from './AgingReport';
|
||||
import { AgingSummaryRowType } from './_constants';
|
||||
import { FinancialTable } from '../FinancialTable';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
|
||||
export default abstract class AgingSummaryTable extends R.compose(
|
||||
FinancialSheetStructure,
|
||||
FinancialTable
|
||||
)(AgingReport) {
|
||||
protected readonly report: IAgingSummaryData;
|
||||
protected readonly query: IAgingSummaryQuery;
|
||||
protected readonly agingPeriods: IAgingPeriod[];
|
||||
protected readonly i18n: any;
|
||||
|
||||
/**
|
||||
* Constructor method.
|
||||
* @param {IARAgingSummaryData} data
|
||||
* @param {IAgingSummaryQuery} query
|
||||
* @param {any} i18n
|
||||
*/
|
||||
constructor(data: IAgingSummaryData, query: IAgingSummaryQuery, i18n: any) {
|
||||
super();
|
||||
|
||||
this.report = data;
|
||||
this.i18n = i18n;
|
||||
this.query = query;
|
||||
|
||||
this.agingPeriods = this.agingRangePeriods(
|
||||
this.query.asDate,
|
||||
this.query.agingDaysBefore,
|
||||
this.query.agingPeriods
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// # Accessors.
|
||||
// -------------------------
|
||||
/**
|
||||
* Aging accessors of contact and total nodes.
|
||||
* @param {IAgingSummaryContact | IAgingSummaryTotal} node
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected agingNodeAccessors = (
|
||||
node: IAgingSummaryContact | IAgingSummaryTotal
|
||||
): ITableColumnAccessor[] => {
|
||||
return node.aging.map((aging, index) => ({
|
||||
key: 'aging',
|
||||
accessor: `aging[${index}].total.formattedAmount`,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Contact name node accessor.
|
||||
* @returns {ITableColumnAccessor}
|
||||
*/
|
||||
protected get contactNameNodeAccessor(): ITableColumnAccessor {
|
||||
return { key: 'customer_name', accessor: 'customerName' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the common columns for all report nodes.
|
||||
* @param {IAgingSummaryContact}
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected contactNodeAccessors = (
|
||||
node: IAgingSummaryContact
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.concat([
|
||||
this.contactNameNodeAccessor,
|
||||
{ key: 'current', accessor: 'current.formattedAmount' },
|
||||
...this.agingNodeAccessors(node),
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
])
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table row.
|
||||
* @param {IAgingSummaryContact} node -
|
||||
* @return {ITableRow}
|
||||
*/
|
||||
protected contactNameNode = (node: IAgingSummaryContact): ITableRow => {
|
||||
const columns = this.contactNodeAccessors(node);
|
||||
const meta = {
|
||||
rowTypes: [AgingSummaryRowType.Contact],
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the customers nodes to table rows.
|
||||
* @param {IAgingSummaryContact[]} nodes
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
protected contactsNodes = (nodes: IAgingSummaryContact[]): ITableRow[] => {
|
||||
return nodes.map(this.contactNameNode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the common columns for all report nodes.
|
||||
* @param {IAgingSummaryTotal}
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
protected totalNodeAccessors = (
|
||||
node: IAgingSummaryTotal
|
||||
): ITableColumnAccessor[] => {
|
||||
return R.compose(
|
||||
R.concat([
|
||||
{ key: 'blank', value: '' },
|
||||
{ key: 'current', accessor: 'current.formattedAmount' },
|
||||
...this.agingNodeAccessors(node),
|
||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||
])
|
||||
)([]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the total row of the given report total node.
|
||||
* @param {IAgingSummaryTotal} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected totalNode = (node: IAgingSummaryTotal): ITableRow => {
|
||||
const columns = this.totalNodeAccessors(node);
|
||||
const meta = {
|
||||
rowTypes: [AgingSummaryRowType.Total],
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Computed Rows.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieves the contacts table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
protected get contactsRows(): ITableRow[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Table total row.
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
protected get totalRow(): ITableRow {
|
||||
return this.totalNode(this.report.total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows = (): ITableRow[] => {
|
||||
return R.compose(
|
||||
R.unless(R.isEmpty, R.append(this.totalRow)),
|
||||
R.concat(this.contactsRows)
|
||||
)([]);
|
||||
};
|
||||
|
||||
// -------------------------
|
||||
// # Columns.
|
||||
// -------------------------
|
||||
/**
|
||||
* Retrieves the aging table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected agingTableColumns = (): ITableColumn[] => {
|
||||
return this.agingPeriods.map((agingPeriod) => {
|
||||
return {
|
||||
label: `${agingPeriod.beforeDays} - ${
|
||||
agingPeriod.toDays || 'And Over'
|
||||
}`,
|
||||
key: 'aging_period',
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the contact name table column.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
protected contactNameTableColumn = (): ITableColumn => {
|
||||
return { label: 'Customer name', key: 'customer_name' };
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the report columns.
|
||||
* @returns {ITableColumn}
|
||||
*/
|
||||
public tableColumns = (): ITableColumn[] => {
|
||||
return R.compose(this.tableColumnsCellIndexing)([
|
||||
this.contactNameTableColumn(),
|
||||
{ label: 'Current', key: 'current' },
|
||||
...this.agingTableColumns(),
|
||||
{ label: 'Total', key: 'total' },
|
||||
]);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum AgingSummaryRowType {
|
||||
Contact = 'contact',
|
||||
Total = 'total',
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export default class BalanceSheetStatementService
|
||||
displayColumnsBy: 'month',
|
||||
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class CashFlowStatementService
|
||||
displayColumnsType: 'total',
|
||||
displayColumnsBy: 'day',
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
|
||||
@@ -12,7 +12,7 @@ export const FinancialTable = (Base) =>
|
||||
* @param {ITableColumn[]} columns
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
protected tableColumnsCellIndexing = (
|
||||
public tableColumnsCellIndexing = (
|
||||
columns: ITableColumn[]
|
||||
): ITableColumn[] => {
|
||||
const cellIndex = increment(-1);
|
||||
|
||||
@@ -31,8 +31,8 @@ export default class GeneralLedgerService {
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
basis: 'cash',
|
||||
numberFormat: {
|
||||
noCents: false,
|
||||
|
||||
@@ -16,13 +16,13 @@ import { Tenant } from '@/system/models';
|
||||
@Service()
|
||||
export default class InventoryDetailsService extends FinancialSheet {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
private tenancy: TenancyService;
|
||||
|
||||
@Inject()
|
||||
reportRepo: InventoryDetailsRepository;
|
||||
private reportRepo: InventoryDetailsRepository;
|
||||
|
||||
@Inject()
|
||||
inventoryService: InventoryService;
|
||||
private inventoryService: InventoryService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
@@ -30,8 +30,8 @@ export default class InventoryDetailsService extends FinancialSheet {
|
||||
*/
|
||||
private get defaultQuery(): IInventoryDetailsQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class InventoryValuationSheetService {
|
||||
*/
|
||||
get defaultQuery(): IInventoryValuationReportQuery {
|
||||
return {
|
||||
asDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
asDate: moment().format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
|
||||
@@ -25,8 +25,8 @@ export default class JournalSheetService {
|
||||
*/
|
||||
get defaultQuery() {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
fromRange: null,
|
||||
toRange: null,
|
||||
accountsIds: [],
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IProfitLossSheetQuery } from '@/interfaces';
|
||||
*/
|
||||
export const getDefaultPLQuery = (): IProfitLossSheetQuery => ({
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
|
||||
numberFormat: {
|
||||
divideOn1000: false,
|
||||
|
||||
@@ -12,10 +12,7 @@ import { Tenant } from '@/system/models';
|
||||
@Service()
|
||||
export default class InventoryValuationReportService {
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
private tenancy: TenancyService;
|
||||
|
||||
/**
|
||||
* Defaults balance sheet filter query.
|
||||
@@ -23,8 +20,8 @@ export default class InventoryValuationReportService {
|
||||
*/
|
||||
get defaultQuery(): IInventoryValuationReportQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
@@ -73,9 +70,9 @@ export default class InventoryValuationReportService {
|
||||
tenantId: number,
|
||||
query: IInventoryValuationReportQuery
|
||||
): Promise<{
|
||||
data: IInventoryValuationStatement,
|
||||
query: IInventoryValuationReportQuery,
|
||||
meta: IInventoryValuationSheetMeta,
|
||||
data: IInventoryValuationStatement;
|
||||
query: IInventoryValuationReportQuery;
|
||||
meta: IInventoryValuationSheetMeta;
|
||||
}> {
|
||||
const { Item, InventoryTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -87,7 +84,7 @@ export default class InventoryValuationReportService {
|
||||
...this.defaultQuery,
|
||||
...query,
|
||||
};
|
||||
const inventoryItems = await Item.query().onBuild(q => {
|
||||
const inventoryItems = await Item.query().onBuild((q) => {
|
||||
q.where('type', 'inventory');
|
||||
|
||||
if (filter.itemsIds.length > 0) {
|
||||
@@ -106,7 +103,7 @@ export default class InventoryValuationReportService {
|
||||
builder.whereIn('itemId', inventoryItemsIds);
|
||||
|
||||
// Filter the date range of the sheet.
|
||||
builder.modify('filterDateRange', filter.fromDate, filter.toDate)
|
||||
builder.modify('filterDateRange', filter.fromDate, filter.toDate);
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ export default class SalesByItemsReportService {
|
||||
*/
|
||||
get defaultQuery(): ISalesByItemsReportQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
itemsIds: [],
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { ITaxRate } from '@/interfaces';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryPayableById,
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
SalesTaxLiabilitySummaryRate,
|
||||
SalesTaxLiabilitySummaryReportData,
|
||||
SalesTaxLiabilitySummarySalesById,
|
||||
SalesTaxLiabilitySummaryTotal,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export class SalesTaxLiabilitySummary extends FinancialSheet {
|
||||
private query: SalesTaxLiabilitySummaryQuery;
|
||||
private taxRates: ITaxRate[];
|
||||
private payableTaxesById: SalesTaxLiabilitySummaryPayableById;
|
||||
private salesTaxesById: SalesTaxLiabilitySummarySalesById;
|
||||
|
||||
/**
|
||||
* Sales tax liability summary constructor.
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @param {ITaxRate[]} taxRates
|
||||
* @param {SalesTaxLiabilitySummaryPayableById} payableTaxesById
|
||||
* @param {SalesTaxLiabilitySummarySalesById} salesTaxesById
|
||||
*/
|
||||
constructor(
|
||||
query: SalesTaxLiabilitySummaryQuery,
|
||||
taxRates: ITaxRate[],
|
||||
payableTaxesById: SalesTaxLiabilitySummaryPayableById,
|
||||
salesTaxesById: SalesTaxLiabilitySummarySalesById
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.taxRates = taxRates;
|
||||
this.payableTaxesById = payableTaxesById;
|
||||
this.salesTaxesById = salesTaxesById;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rate liability node.
|
||||
* @param {ITaxRate} taxRate
|
||||
* @returns {SalesTaxLiabilitySummaryRate}
|
||||
*/
|
||||
private taxRateLiability = (
|
||||
taxRate: ITaxRate
|
||||
): SalesTaxLiabilitySummaryRate => {
|
||||
const payableTax = this.payableTaxesById[taxRate.id];
|
||||
const salesTax = this.salesTaxesById[taxRate.id];
|
||||
|
||||
const payableTaxAmount = payableTax
|
||||
? payableTax.credit - payableTax.debit
|
||||
: 0;
|
||||
const salesTaxAmount = salesTax ? salesTax.credit - salesTax.debit : 0;
|
||||
|
||||
// Calculates the tax percentage.
|
||||
const taxPercentage = R.compose(
|
||||
R.unless(R.equals(0), R.divide(R.__, salesTaxAmount))
|
||||
)(payableTaxAmount);
|
||||
|
||||
// Calculates the payable tax amount.
|
||||
const collectedTaxAmount = payableTax ? payableTax.debit : 0;
|
||||
|
||||
return {
|
||||
id: taxRate.id,
|
||||
taxName: `${taxRate.name} (${taxRate.rate}%)`,
|
||||
taxableAmount: this.getAmountMeta(salesTaxAmount),
|
||||
taxAmount: this.getAmountMeta(payableTaxAmount),
|
||||
taxPercentage: this.getPercentageTotalAmountMeta(taxPercentage),
|
||||
collectedTaxAmount: this.getAmountMeta(collectedTaxAmount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the non-transactions tax rates.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||
*/
|
||||
private filterNonTransactionsTaxRates = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): SalesTaxLiabilitySummaryRate[] => {
|
||||
return nodes.filter((node) => {
|
||||
const salesTrxs = this.salesTaxesById[node.id];
|
||||
const payableTrxs = this.payableTaxesById[node.id];
|
||||
|
||||
return !isEmpty(salesTrxs) || !isEmpty(payableTrxs);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates liability nodes.
|
||||
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||
*/
|
||||
private taxRatesLiability = (): SalesTaxLiabilitySummaryRate[] => {
|
||||
return R.compose(
|
||||
this.filterNonTransactionsTaxRates,
|
||||
R.map(this.taxRateLiability)
|
||||
)(this.taxRates);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates total node.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {SalesTaxLiabilitySummaryTotal}
|
||||
*/
|
||||
private taxRatesTotal = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): SalesTaxLiabilitySummaryTotal => {
|
||||
const taxableAmount = sumBy(nodes, 'taxableAmount.amount');
|
||||
const taxAmount = sumBy(nodes, 'taxAmount.amount');
|
||||
const collectedTaxAmount = sumBy(nodes, 'collectedTaxAmount.amount');
|
||||
|
||||
return {
|
||||
taxableAmount: this.getTotalAmountMeta(taxableAmount),
|
||||
taxAmount: this.getTotalAmountMeta(taxAmount),
|
||||
collectedTaxAmount: this.getTotalAmountMeta(collectedTaxAmount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the report data.
|
||||
* @returns {SalesTaxLiabilitySummaryReportData}
|
||||
*/
|
||||
public reportData = (): SalesTaxLiabilitySummaryReportData => {
|
||||
const taxRates = this.taxRatesLiability();
|
||||
const total = this.taxRatesTotal(taxRates);
|
||||
|
||||
return { taxRates, total };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryPayableById,
|
||||
SalesTaxLiabilitySummarySalesById,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { keyBy } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export class SalesTaxLiabilitySummaryRepository {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve tax rates.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<TaxRate[]>}
|
||||
*/
|
||||
public taxRates = (tenantId: number) => {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
return TaxRate.query().orderBy('name', 'desc');
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve taxes payable sum grouped by tax rate id.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<SalesTaxLiabilitySummaryPayableById>}
|
||||
*/
|
||||
public async taxesPayableSumGroupedByRateId(
|
||||
tenantId: number
|
||||
): Promise<SalesTaxLiabilitySummaryPayableById> {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves tax payable accounts.
|
||||
const taxPayableAccounts = await Account.query().whereIn('accountType', [
|
||||
ACCOUNT_TYPE.TAX_PAYABLE,
|
||||
]);
|
||||
const payableAccountsIds = taxPayableAccounts.map((account) => account.id);
|
||||
|
||||
const groupedTaxesById = await AccountTransaction.query()
|
||||
.whereIn('account_id', payableAccountsIds)
|
||||
.whereNot('tax_rate_id', null)
|
||||
.groupBy('tax_rate_id')
|
||||
.select(['tax_rate_id'])
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit');
|
||||
|
||||
return keyBy(groupedTaxesById, 'taxRateId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve taxes sales sum grouped by tax rate id.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<SalesTaxLiabilitySummarySalesById>}
|
||||
*/
|
||||
public taxesSalesSumGroupedByRateId = async (
|
||||
tenantId: number
|
||||
): Promise<SalesTaxLiabilitySummarySalesById> => {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const incomeAccounts = await Account.query().whereIn('accountType', [
|
||||
ACCOUNT_TYPE.INCOME,
|
||||
ACCOUNT_TYPE.OTHER_INCOME,
|
||||
]);
|
||||
const incomeAccountsIds = incomeAccounts.map((account) => account.id);
|
||||
|
||||
const groupedTaxesById = await AccountTransaction.query()
|
||||
.whereIn('account_id', incomeAccountsIds)
|
||||
.whereNot('tax_rate_id', null)
|
||||
.groupBy('tax_rate_id')
|
||||
.select(['tax_rate_id'])
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit');
|
||||
|
||||
return keyBy(groupedTaxesById, 'taxRateId');
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryMeta,
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary';
|
||||
import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class SalesTaxLiabilitySummaryService {
|
||||
@Inject()
|
||||
private repostiory: SalesTaxLiabilitySummaryRepository;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve sales tax liability summary.
|
||||
* @param {number} tenantId
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @returns
|
||||
*/
|
||||
public async salesTaxLiability(
|
||||
tenantId: number,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
const payableByRateId =
|
||||
await this.repostiory.taxesPayableSumGroupedByRateId(tenantId);
|
||||
|
||||
const salesByRateId = await this.repostiory.taxesSalesSumGroupedByRateId(
|
||||
tenantId
|
||||
);
|
||||
const taxRates = await this.repostiory.taxRates(tenantId);
|
||||
|
||||
const taxLiabilitySummary = new SalesTaxLiabilitySummary(
|
||||
query,
|
||||
taxRates,
|
||||
payableByRateId,
|
||||
salesByRateId
|
||||
);
|
||||
return {
|
||||
data: taxLiabilitySummary.reportData(),
|
||||
query,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sales tax liability summary table.
|
||||
* @param {number} tenantId
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @returns
|
||||
*/
|
||||
public async salesTaxLiabilitySummaryTable(
|
||||
tenantId: number,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
const report = await this.salesTaxLiability(tenantId, query);
|
||||
|
||||
// Creates the sales tax liability summary table.
|
||||
const table = new SalesTaxLiabilitySummaryTable(report.data, query);
|
||||
|
||||
return {
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
data: report.data,
|
||||
query: report.query,
|
||||
meta: report.meta,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): SalesTaxLiabilitySummaryMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
SalesTaxLiabilitySummaryRate,
|
||||
SalesTaxLiabilitySummaryReportData,
|
||||
SalesTaxLiabilitySummaryTotal,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import { tableRowMapper } from '@/utils';
|
||||
import { ITableColumn, ITableRow } from '@/interfaces';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import { FinancialTable } from '../FinancialTable';
|
||||
import AgingReport from '../AgingSummary/AgingReport';
|
||||
import { IROW_TYPE } from './_constants';
|
||||
|
||||
export class SalesTaxLiabilitySummaryTable extends R.compose(
|
||||
FinancialSheetStructure,
|
||||
FinancialTable
|
||||
)(AgingReport) {
|
||||
private data: SalesTaxLiabilitySummaryReportData;
|
||||
private query: SalesTaxLiabilitySummaryQuery;
|
||||
|
||||
/**
|
||||
* Sales tax liability summary table constructor.
|
||||
* @param {SalesTaxLiabilitySummaryReportData} data
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
*/
|
||||
constructor(
|
||||
data: SalesTaxLiabilitySummaryReportData,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
super();
|
||||
|
||||
this.data = data;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tax rate row accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
private get taxRateRowAccessor() {
|
||||
return [
|
||||
{ key: 'taxName', accessor: 'taxName' },
|
||||
{ key: 'taxPercentage', accessor: 'taxPercentage.formattedAmount' },
|
||||
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tax rate total row accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
private get taxRateTotalRowAccessors() {
|
||||
return [
|
||||
{ key: 'taxName', value: 'Total' },
|
||||
{ key: 'taxPercentage', value: '' },
|
||||
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the tax rate node to table row.
|
||||
* @param {SalesTaxLiabilitySummaryRate} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private taxRateTableRowMapper = (
|
||||
node: SalesTaxLiabilitySummaryRate
|
||||
): ITableRow => {
|
||||
const columns = this.taxRateRowAccessor;
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.TaxRate],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the tax rates nodes to table rows.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private taxRatesTableRowsMapper = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): ITableRow[] => {
|
||||
return nodes.map(this.taxRateTableRowMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the tax rate total node to table row.
|
||||
* @param {SalesTaxLiabilitySummaryTotal} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private taxRateTotalRowMapper = (node: SalesTaxLiabilitySummaryTotal) => {
|
||||
const columns = this.taxRateTotalRowAccessors;
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.Total],
|
||||
id: node.key,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rate total row.
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private get taxRateTotalRow(): ITableRow {
|
||||
return this.taxRateTotalRowMapper(this.data.total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private get taxRatesRows(): ITableRow[] {
|
||||
return this.taxRatesTableRowsMapper(this.data.taxRates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows(): ITableRow[] {
|
||||
return R.compose(
|
||||
R.unless(R.isEmpty, R.append(this.taxRateTotalRow)),
|
||||
R.concat(this.taxRatesRows)
|
||||
)([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns(): ITableColumn[] {
|
||||
return R.compose(this.tableColumnsCellIndexing)([
|
||||
{
|
||||
label: 'Tax Name',
|
||||
key: 'taxName',
|
||||
},
|
||||
{
|
||||
label: 'Tax Percentage',
|
||||
key: 'taxPercentage',
|
||||
},
|
||||
{
|
||||
label: 'Taxable Amount',
|
||||
key: 'taxableAmount',
|
||||
},
|
||||
{
|
||||
label: 'Collected Tax',
|
||||
key: 'collectedTax',
|
||||
},
|
||||
{
|
||||
label: 'Tax Amount',
|
||||
key: 'taxRate',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum IROW_TYPE {
|
||||
TaxRate = 'TaxRate',
|
||||
Total = 'Total',
|
||||
}
|
||||
@@ -31,8 +31,8 @@ export default class TransactionsByCustomersService
|
||||
*/
|
||||
get defaultQuery(): ITransactionsByCustomersFilter {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
divideOn1000: false,
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class TransactionsByVendorsService
|
||||
*/
|
||||
get defaultQuery(): ITransactionsByVendorsFilter {
|
||||
return {
|
||||
fromDate: moment().format('YYYY-MM-DD'),
|
||||
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
precision: 2,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class TrialBalanceSheetService extends FinancialSheet {
|
||||
get defaultQuery(): ITrialBalanceSheetQuery {
|
||||
return {
|
||||
fromDate: moment().startOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().endOf('year').format('YYYY-MM-DD'),
|
||||
toDate: moment().format('YYYY-MM-DD'),
|
||||
numberFormat: {
|
||||
divideOn1000: false,
|
||||
negativeFormat: 'mines',
|
||||
|
||||
@@ -9,13 +9,9 @@ import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
import events from '@/subscribers/events';
|
||||
import { ERRORS } from './constants';
|
||||
import { ItemsValidators } from './ItemValidators';
|
||||
|
||||
@Service()
|
||||
export class DeleteItem {
|
||||
@Inject()
|
||||
private validators: ItemsValidators;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
|
||||
export default class ItemsCostService {
|
||||
|
||||
}
|
||||
@@ -264,4 +264,13 @@ export default class ItemsEntriesService {
|
||||
public getTotalItemsEntries(entries: ItemEntry[]): number {
|
||||
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the non-zero tax items entries.
|
||||
* @param {IItemEntry[]} entries -
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
public getNonZeroEntries(entries: IItemEntry[]): IItemEntry[] {
|
||||
return entries.filter((e) => e.taxRate > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import Knex from 'knex';
|
||||
import { Knex } from 'knex';
|
||||
import Bluebird from 'bluebird';
|
||||
import { IVendorCreditAppliedBill } from '@/interfaces';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
@Service()
|
||||
export default class ApplyVendorCreditSyncBills {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Increment bills credited amount.
|
||||
@@ -49,4 +49,4 @@ export default class ApplyVendorCreditSyncBills {
|
||||
.findById(vendorCreditAppliedBill.billId)
|
||||
.decrement('creditedAmount', vendorCreditAppliedBill.amount);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,14 @@ import {
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||
import { ItemEntry } from '@/models';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@@ -38,6 +36,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private invoiceIncrement: SaleInvoiceIncrement;
|
||||
|
||||
@Inject()
|
||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||
|
||||
/**
|
||||
* Transformes the create DTO to invoice object model.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||
@@ -51,11 +52,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
authorizedUser: ITenantUser,
|
||||
oldSaleInvoice?: ISaleInvoice
|
||||
): Promise<ISaleInvoice> {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO);
|
||||
const amount = this.getDueBalanceItemEntries(entriesModels);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
|
||||
ItemEntry.calcAmount(e)
|
||||
);
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId);
|
||||
|
||||
@@ -68,20 +67,30 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
|
||||
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
|
||||
referenceType: 'SaleInvoice',
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
...entry,
|
||||
}));
|
||||
const entries = await composeAsync(
|
||||
const asyncEntries = await composeAsync(
|
||||
// Associate tax rate from tax id to entries.
|
||||
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries(tenantId),
|
||||
// Associate tax rate id from tax code to entries.
|
||||
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
|
||||
// Sets default cost and sell account to invoice items entries.
|
||||
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
|
||||
)(initialEntries);
|
||||
|
||||
const entries = R.compose(
|
||||
// Remove tax code from entries.
|
||||
R.map(R.omit(['taxCode']))
|
||||
)(asyncEntries);
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(
|
||||
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
|
||||
['invoiceDate', 'dueDate']
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
balance,
|
||||
balance: amount,
|
||||
currencyCode: customer.currencyCode,
|
||||
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
|
||||
...(saleInvoiceDTO.delivered &&
|
||||
@@ -96,8 +105,34 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
} as ISaleInvoice;
|
||||
|
||||
return R.compose(
|
||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||
)(initialDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the DTO entries to invoice entries models.
|
||||
* @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private transformDTOEntriesToModels = (
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO
|
||||
): ItemEntry[] => {
|
||||
return saleInvoiceDTO.entries.map((entry) => {
|
||||
return ItemEntry.fromJson({
|
||||
...entry,
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the due balance from the invoice entries.
|
||||
* @param {IItemEntry[]} entries
|
||||
* @returns {number}
|
||||
*/
|
||||
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
|
||||
return sumBy(entries, (e) => e.amount);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ export class GetSaleInvoice {
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('entries.tax')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch');
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('taxes.taxRate');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
IItemEntry,
|
||||
@@ -6,11 +7,11 @@ import {
|
||||
AccountNormal,
|
||||
ILedger,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceGLEntries {
|
||||
@@ -20,10 +21,13 @@ export class SaleInvoiceGLEntries {
|
||||
@Inject()
|
||||
private ledegrRepository: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
/**
|
||||
* Writes a sale invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeInvoiceGLEntries = async (
|
||||
@@ -42,9 +46,17 @@ export class SaleInvoiceGLEntries {
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode
|
||||
);
|
||||
// Find or create tax payable account.
|
||||
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Retrieves the ledger of the invoice.
|
||||
const ledger = this.getInvoiceGLedger(saleInvoice, ARAccount.id);
|
||||
|
||||
const ledger = this.getInvoiceGLedger(
|
||||
saleInvoice,
|
||||
ARAccount.id,
|
||||
taxPayableAccount.id
|
||||
);
|
||||
// Commits the ledger entries to the storage as UOW.
|
||||
await this.ledegrRepository.commit(tenantId, ledger, trx);
|
||||
};
|
||||
@@ -94,10 +106,14 @@ export class SaleInvoiceGLEntries {
|
||||
*/
|
||||
public getInvoiceGLedger = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
): ILedger => {
|
||||
const entries = this.getInvoiceGLEntries(saleInvoice, ARAccountId);
|
||||
|
||||
const entries = this.getInvoiceGLEntries(
|
||||
saleInvoice,
|
||||
ARAccountId,
|
||||
taxPayableAccountId
|
||||
);
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
@@ -143,7 +159,7 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleInvoice.localAmount,
|
||||
debit: saleInvoice.totalLocal,
|
||||
accountId: ARAccountId,
|
||||
contactId: saleInvoice.customerId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
@@ -165,7 +181,7 @@ export class SaleInvoiceGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
const localAmount = entry.amount * saleInvoice.exchangeRate;
|
||||
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
@@ -176,11 +192,62 @@ export class SaleInvoiceGLEntries {
|
||||
itemId: entry.itemId,
|
||||
itemQuantity: entry.quantity,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
projectId: entry.projectId || saleInvoice.projectId
|
||||
projectId: entry.projectId || saleInvoice.projectId,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retreives the GL entry of tax payable.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {number} taxPayableAccountId -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceTaxEntry = R.curry(
|
||||
(
|
||||
saleInvoice: ISaleInvoice,
|
||||
taxPayableAccountId: number,
|
||||
entry: IItemEntry,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: entry.taxAmount,
|
||||
accountId: taxPayableAccountId,
|
||||
index: index + 3,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the invoice tax GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} taxPayableAccountId
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getInvoiceTaxEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
taxPayableAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
// Retrieves the non-zero tax entries.
|
||||
const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries(
|
||||
saleInvoice.entries
|
||||
);
|
||||
const transformTaxEntry = this.getInvoiceTaxEntry(
|
||||
saleInvoice,
|
||||
taxPayableAccountId
|
||||
);
|
||||
// Transforms the non-zero tax entries to GL entries.
|
||||
return nonZeroTaxEntries.map(transformTaxEntry);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
@@ -189,7 +256,8 @@ export class SaleInvoiceGLEntries {
|
||||
*/
|
||||
public getInvoiceGLEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const receivableEntry = this.getInvoiceReceivableEntry(
|
||||
saleInvoice,
|
||||
@@ -198,6 +266,10 @@ export class SaleInvoiceGLEntries {
|
||||
const transformItemEntry = this.getInvoiceItemEntry(saleInvoice);
|
||||
const creditEntries = saleInvoice.entries.map(transformItemEntry);
|
||||
|
||||
return [receivableEntry, ...creditEntries];
|
||||
const taxEntries = this.getInvoiceTaxEntries(
|
||||
saleInvoice,
|
||||
taxPayableAccountId
|
||||
);
|
||||
return [receivableEntry, ...creditEntries, ...taxEntries];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from '@/utils';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
|
||||
import { format } from 'mathjs';
|
||||
|
||||
export class SaleInvoiceTaxEntryTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'taxRateCode',
|
||||
'taxRate',
|
||||
'taxRateId',
|
||||
'taxRateAmount',
|
||||
'taxRateAmountFormatted',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate code.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateCode = (taxEntry) => {
|
||||
return taxEntry.taxRate.code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate id.
|
||||
* @param taxEntry
|
||||
* @returns {number}
|
||||
*/
|
||||
protected taxRate = (taxEntry) => {
|
||||
return taxEntry.taxAmount || taxEntry.taxRate.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate name.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected name = (taxEntry) => {
|
||||
return taxEntry.taxRate.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate amount.
|
||||
* @param taxEntry
|
||||
*/
|
||||
protected taxRateAmount = (taxEntry) => {
|
||||
const taxRate = this.taxRate(taxEntry);
|
||||
|
||||
return this.options.isInclusiveTax
|
||||
? getInclusiveTaxAmount(this.options.amount, taxRate)
|
||||
: getExlusiveTaxAmount(this.options.amount, taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted tax rate amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateAmountFormatted = (taxEntry) => {
|
||||
return formatNumber(this.taxRateAmount(taxEntry), {
|
||||
currencyCode: this.options.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
@@ -8,13 +9,20 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedInvoiceDate',
|
||||
'formattedDueDate',
|
||||
'formattedAmount',
|
||||
'formattedDueAmount',
|
||||
'formattedPaymentAmount',
|
||||
'formattedBalanceAmount',
|
||||
'formattedExchangeRate',
|
||||
'invoiceDateFormatted',
|
||||
'dueDateFormatted',
|
||||
'dueAmountFormatted',
|
||||
'paymentAmountFormatted',
|
||||
'balanceAmountFormatted',
|
||||
'exchangeRateFormatted',
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
'subtotalExludingTaxFormatted',
|
||||
'taxAmountWithheldFormatted',
|
||||
'taxAmountWithheldLocalFormatted',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'taxes',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -23,7 +31,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedInvoiceDate = (invoice): string => {
|
||||
protected invoiceDateFormatted = (invoice): string => {
|
||||
return this.formatDate(invoice.invoiceDate);
|
||||
};
|
||||
|
||||
@@ -32,27 +40,16 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueDate = (invoice): string => {
|
||||
protected dueDateFormatted = (invoice): string => {
|
||||
return this.formatDate(invoice.dueDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (invoice): string => {
|
||||
return formatNumber(invoice.balance, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice due amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueAmount = (invoice): string => {
|
||||
protected dueAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -63,7 +60,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentAmount = (invoice): string => {
|
||||
protected paymentAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.paymentAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -74,7 +71,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedBalanceAmount = (invoice): string => {
|
||||
protected balanceAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.balanceAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -85,7 +82,98 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedExchangeRate = (invoice): string => {
|
||||
protected exchangeRateFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.exchangeRate, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in base currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in foreign currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotalLocal, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal excluding tax in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalExludingTaxFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotalExludingTax, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.taxAmountWithheld, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.taxAmountWithheldLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.total, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.totalLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the taxes lines of sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
*/
|
||||
protected taxes = (invoice) => {
|
||||
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
|
||||
amount: invoice.amount,
|
||||
isInclusiveTax: invoice.isInclusiveTax,
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class PaymentReceivesApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes the given payment receive.
|
||||
* Deletes the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
@@ -126,7 +126,7 @@ export class PaymentReceivesApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @returns {Promise<IPaymentReceive>}
|
||||
|
||||
67
packages/server/src/services/TaxRates/ActivateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/ActivateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ITaxRateActivatedPayload,
|
||||
ITaxRateActivatingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class ActivateTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Activates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public activateTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
// Validates the tax rate inactive.
|
||||
this.validators.validateTaxRateNotActive(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateActivating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onActivating, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx)
|
||||
.findById(taxRateId)
|
||||
.patch({ active: 1 });
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onActivated, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { IItemEntryDTO, ITaxRate } from '@/interfaces';
|
||||
import { ERRORS } from './constants';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class CommandTaxRatesValidators {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validates the tax rate existance.
|
||||
* @param {TaxRate | undefined | null} taxRate
|
||||
*/
|
||||
public validateTaxRateExistance(taxRate: ITaxRate | undefined | null) {
|
||||
if (!taxRate) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given tax rate active.
|
||||
* @param {ITaxRate} taxRate
|
||||
*/
|
||||
public validateTaxRateNotActive(taxRate: ITaxRate) {
|
||||
if (taxRate.active) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_ALREADY_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given tax rate inactive.
|
||||
* @param {ITaxRate} taxRate
|
||||
*/
|
||||
public validateTaxRateNotInactive(taxRate: ITaxRate) {
|
||||
if (!taxRate.active) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_ALREADY_INACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax code uniquiness.
|
||||
* @param {number} tenantId
|
||||
* @param {string} taxCode
|
||||
*/
|
||||
public async validateTaxCodeUnique(tenantId: number, taxCode: string) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundTaxCode = await TaxRate.query().findOne({ code: taxCode });
|
||||
|
||||
if (foundTaxCode) {
|
||||
throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax codes of the given item entries DTO.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateItemEntriesTaxCode(
|
||||
tenantId: number,
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode);
|
||||
const taxCodes = filteredTaxEntries.map((e) => e.taxCode);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxCodes.length === 0) return;
|
||||
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
const foundCodes = foundTaxCodes.map((tax) => tax.code);
|
||||
|
||||
const notFoundTaxCodes = difference(taxCodes, foundCodes);
|
||||
|
||||
if (notFoundTaxCodes.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax rate id of the given item entries DTO.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateItemEntriesTaxCodeId(
|
||||
tenantId: number,
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxRateId);
|
||||
const taxRatesIds = filteredTaxEntries.map((e) => e.taxRateId);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxRatesIds.length === 0) return;
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('id', taxRatesIds);
|
||||
const foundTaxRatesIds = foundTaxCodes.map((tax) => tax.id);
|
||||
|
||||
const notFoundTaxCodes = difference(taxRatesIds, foundTaxRatesIds);
|
||||
|
||||
if (notFoundTaxCodes.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/TaxRates/CreateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/CreateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ICreateTaxRateDTO,
|
||||
ITaxRateCreatedPayload,
|
||||
ITaxRateCreatingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
|
||||
@Service()
|
||||
export class CreateTaxRate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreateTaxRateDTO} createTaxRateDTO
|
||||
*/
|
||||
public async createTaxRate(
|
||||
tenantId: number,
|
||||
createTaxRateDTO: ICreateTaxRateDTO
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validates the tax code uniquiness.
|
||||
await this.validators.validateTaxCodeUnique(
|
||||
tenantId,
|
||||
createTaxRateDTO.code
|
||||
);
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onCreating, {
|
||||
createTaxRateDTO,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateCreatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx).insertAndFetch({
|
||||
...createTaxRateDTO,
|
||||
});
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onCreated, {
|
||||
createTaxRateDTO,
|
||||
taxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateCreatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
56
packages/server/src/services/TaxRates/DeleteTaxRate.ts
Normal file
56
packages/server/src/services/TaxRates/DeleteTaxRate.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { ITaxRateDeletedPayload, ITaxRateDeletingPayload } from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class DeleteTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onDeleting, {
|
||||
oldTaxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateDeletingPayload);
|
||||
|
||||
await TaxRate.query(trx).findById(taxRateId).delete();
|
||||
|
||||
// Triggers `onTaxRateDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onDeleted, {
|
||||
oldTaxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateDeletedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
126
packages/server/src/services/TaxRates/EditTaxRate.ts
Normal file
126
packages/server/src/services/TaxRates/EditTaxRate.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
IEditTaxRateDTO,
|
||||
ITaxRate,
|
||||
ITaxRateEditedPayload,
|
||||
ITaxRateEditingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class EditTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Detarmines whether the tax rate, name or code have been changed.
|
||||
* @param {ITaxRate} taxRate
|
||||
* @param {IEditTaxRateDTO} editTaxRateDTO
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isTaxRateDTOChanged = (
|
||||
taxRate: ITaxRate,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) => {
|
||||
return (
|
||||
taxRate.rate !== editTaxRateDTO.rate ||
|
||||
taxRate.name !== editTaxRateDTO.name ||
|
||||
taxRate.code !== editTaxRateDTO.code
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given tax rate or creates a new if the rate or name have been changed.
|
||||
* @param {number} tenantId
|
||||
* @param {ITaxRate} oldTaxRate
|
||||
* @param {IEditTaxRateDTO} editTaxRateDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
private async editTaxRateOrCreate(
|
||||
tenantId: number,
|
||||
oldTaxRate: ITaxRate,
|
||||
editTaxRateDTO: IEditTaxRateDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const isTaxDTOChanged = this.isTaxRateDTOChanged(
|
||||
oldTaxRate,
|
||||
editTaxRateDTO
|
||||
);
|
||||
if (isTaxDTOChanged) {
|
||||
// Soft deleting the old tax rate.
|
||||
await TaxRate.query(trx).findById(oldTaxRate.id).delete();
|
||||
|
||||
// Create a new tax rate with new edited data.
|
||||
return TaxRate.query(trx).insertAndFetch({
|
||||
...omit(oldTaxRate, ['id']),
|
||||
...editTaxRateDTO,
|
||||
});
|
||||
} else {
|
||||
return TaxRate.query(trx).patchAndFetchById(oldTaxRate.id, {
|
||||
...editTaxRateDTO,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async editTaxRate(
|
||||
tenantId: number,
|
||||
taxRateId: number,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onEditing, {
|
||||
editTaxRateDTO,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateEditingPayload);
|
||||
|
||||
const taxRate = await this.editTaxRateOrCreate(
|
||||
tenantId,
|
||||
oldTaxRate,
|
||||
editTaxRateDTO,
|
||||
trx
|
||||
);
|
||||
// Triggers `onTaxRateEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
|
||||
editTaxRateDTO,
|
||||
taxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateEditedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
39
packages/server/src/services/TaxRates/GetTaxRate.ts
Normal file
39
packages/server/src/services/TaxRates/GetTaxRate.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { TaxRateTransformer } from './TaxRateTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async getTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const taxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(taxRate);
|
||||
|
||||
// Transforms the tax rate.
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
taxRate,
|
||||
new TaxRateTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
32
packages/server/src/services/TaxRates/GetTaxRates.ts
Normal file
32
packages/server/src/services/TaxRates/GetTaxRates.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { TaxRateTransformer } from './TaxRateTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetTaxRatesService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ITaxRate[]>}
|
||||
*/
|
||||
public async getTaxRates(tenantId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the tax rates.
|
||||
const taxRates = await TaxRate.query().orderBy('name', 'ASC');
|
||||
|
||||
// Transforms the tax rates.
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
taxRates,
|
||||
new TaxRateTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/TaxRates/InactivateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/InactivateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
ITaxRateActivatedPayload,
|
||||
ITaxRateActivatingPayload,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class InactivateTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async inactivateTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
// Validates the tax rate active.
|
||||
this.validators.validateTaxRateNotInactive(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateActivating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onInactivating, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx)
|
||||
.findById(taxRateId)
|
||||
.patch({ active: 0 });
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onInactivated, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { keyBy, sumBy } from 'lodash';
|
||||
import { ItemEntry } from '@/models';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class ItemEntriesTaxTransactions {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Associates tax amount withheld to the model.
|
||||
* @param model
|
||||
* @returns
|
||||
*/
|
||||
public assocTaxAmountWithheldFromEntries(model: any) {
|
||||
const entries = model.entries.map((entry) => ItemEntry.fromJson(entry));
|
||||
const taxAmountWithheld = sumBy(entries, 'taxAmount');
|
||||
|
||||
if (taxAmountWithheld) {
|
||||
model.taxAmountWithheld = taxAmountWithheld;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates tax rate id from tax code to entries.
|
||||
* @param {number} tenantId
|
||||
* @param {} model
|
||||
*/
|
||||
public assocTaxRateIdFromCodeToEntries =
|
||||
(tenantId: number) => async (entries: any) => {
|
||||
const entriesWithCode = entries.filter((entry) => entry.taxCode);
|
||||
const taxCodes = entriesWithCode.map((entry) => entry.taxCode);
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
|
||||
const taxCodesMap = keyBy(foundTaxCodes, 'code');
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (entry.taxCode) {
|
||||
entry.taxRateId = taxCodesMap[entry.taxCode]?.id;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Associates tax rate from tax id to entries.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<IItemEntry[]>}
|
||||
*/
|
||||
public assocTaxRateFromTaxIdToEntries =
|
||||
(tenantId: number) => async (entries: IItemEntry[]) => {
|
||||
const entriesWithId = entries.filter((e) => e.taxRateId);
|
||||
const taxRateIds = entriesWithId.map((e) => e.taxRateId);
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxes = await TaxRate.query().whereIn('id', taxRateIds);
|
||||
|
||||
const taxRatesMap = keyBy(foundTaxes, 'id');
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (entry.taxRateId) {
|
||||
entry.taxRate = taxRatesMap[entry.taxRateId]?.rate;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
};
|
||||
}
|
||||
29
packages/server/src/services/TaxRates/TaxRateTransformer.ts
Normal file
29
packages/server/src/services/TaxRates/TaxRateTransformer.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class TaxRateTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to tax rate object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['nameFormatted', 'rateFormatted'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted rate.
|
||||
* @param taxRate
|
||||
* @returns {string}
|
||||
*/
|
||||
public rateFormatted = (taxRate): string => {
|
||||
return `${taxRate.rate}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the tax rate name.
|
||||
* @param taxRate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected nameFormatted = (taxRate): string => {
|
||||
return `${taxRate.name} [${taxRate.rate}%]`;
|
||||
};
|
||||
}
|
||||
109
packages/server/src/services/TaxRates/TaxRatesApplication.ts
Normal file
109
packages/server/src/services/TaxRates/TaxRatesApplication.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ICreateTaxRateDTO, IEditTaxRateDTO } from '@/interfaces';
|
||||
import { CreateTaxRate } from './CreateTaxRate';
|
||||
import { DeleteTaxRateService } from './DeleteTaxRate';
|
||||
import { EditTaxRateService } from './EditTaxRate';
|
||||
import { GetTaxRateService } from './GetTaxRate';
|
||||
import { GetTaxRatesService } from './GetTaxRates';
|
||||
import { ActivateTaxRateService } from './ActivateTaxRate';
|
||||
import { InactivateTaxRateService } from './InactivateTaxRate';
|
||||
|
||||
@Service()
|
||||
export class TaxRatesApplication {
|
||||
@Inject()
|
||||
private createTaxRateService: CreateTaxRate;
|
||||
|
||||
@Inject()
|
||||
private editTaxRateService: EditTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private deleteTaxRateService: DeleteTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private getTaxRateService: GetTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private getTaxRatesService: GetTaxRatesService;
|
||||
|
||||
@Inject()
|
||||
private activateTaxRateService: ActivateTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private inactivateTaxRateService: InactivateTaxRateService;
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreateTaxRateDTO} createTaxRateDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public createTaxRate(tenantId: number, createTaxRateDTO: ICreateTaxRateDTO) {
|
||||
return this.createTaxRateService.createTaxRate(tenantId, createTaxRateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public editTaxRate(
|
||||
tenantId: number,
|
||||
taxRateId: number,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) {
|
||||
return this.editTaxRateService.editTaxRate(
|
||||
tenantId,
|
||||
taxRateId,
|
||||
editTaxRateDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.deleteTaxRateService.deleteTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public getTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.getTaxRateService.getTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ITaxRate[]>}
|
||||
*/
|
||||
public getTaxRates(tenantId: number) {
|
||||
return this.getTaxRatesService.getTaxRates(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public activateTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.activateTaxRateService.activateTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public inactivateTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.inactivateTaxRateService.inactivateTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user