Compare commits
63 Commits
v0.9.12
...
@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 | ||
|
|
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']
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -2,6 +2,20 @@
|
||||
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { sumBy, chain, keyBy } from 'lodash';
|
||||
import { IItemEntry, ITaxTransaction } from '@/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class WriteTaxTransactionsItemEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Writes the tax transactions from the given item entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
*/
|
||||
public async writeTaxTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[],
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRateTransaction, TaxRate } = this.tenancy.models(tenantId);
|
||||
const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries);
|
||||
|
||||
const entriesTaxRateIds = aggregatedEntries.map((entry) => entry.taxRateId);
|
||||
|
||||
const taxRates = await TaxRate.query(trx).whereIn('id', entriesTaxRateIds);
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
const taxTransactions = aggregatedEntries.map((entry) => ({
|
||||
taxRateId: entry.taxRateId,
|
||||
referenceType: entry.referenceType,
|
||||
referenceId: entry.referenceId,
|
||||
rate: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
|
||||
})) as ITaxTransaction[];
|
||||
|
||||
await TaxRateTransaction.query(trx).upsertGraph(taxTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the tax rate transactions from the given item entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
* @param {string} referenceType
|
||||
* @param {number} referenceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async rewriteTaxRateTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[],
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
await Promise.all([
|
||||
this.removeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
referenceId,
|
||||
referenceType,
|
||||
trx
|
||||
),
|
||||
this.writeTaxTransactionsFromItemEntries(tenantId, itemEntries, trx),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates by tax code id and sums the amount.
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private aggregateItemEntriesByTaxCode = (
|
||||
itemEntries: IItemEntry[]
|
||||
): IItemEntry[] => {
|
||||
return chain(itemEntries.filter((item) => item.taxRateId))
|
||||
.groupBy((item) => item.taxRateId)
|
||||
.values()
|
||||
.map((group) => ({ ...group[0], amount: sumBy(group, 'amount') }))
|
||||
.value();
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the tax transactions from the given item entries.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
* @param {number} referenceId - Reference id.
|
||||
*/
|
||||
public async removeTaxTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceType: string,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRateTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
await TaxRateTransaction.query(trx)
|
||||
.where({ referenceType, referenceId })
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
8
packages/server/src/services/TaxRates/constants.ts
Normal file
8
packages/server/src/services/TaxRates/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const ERRORS = {
|
||||
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
|
||||
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
|
||||
ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND',
|
||||
ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND',
|
||||
TAX_RATE_ALREADY_ACTIVE: 'TAX_RATE_ALREADY_ACTIVE',
|
||||
TAX_RATE_ALREADY_INACTIVE: 'TAX_RATE_ALREADY_INACTIVE'
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ISaleInvoiceCreatingPaylaod,
|
||||
ISaleInvoiceEditingPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceTaxRateValidateSubscriber {
|
||||
@Inject()
|
||||
private taxRateDTOValidator: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxIdExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEditing,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEditing,
|
||||
this.validateSaleInvoiceEntriesTaxIdExistanceOnEditing
|
||||
);
|
||||
return bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatingPaylaod) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the tax rate id existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxIdExistanceOnCreating = async ({
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatingPaylaod) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance when editing.
|
||||
* @param {ISaleInvoiceEditingPayload}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
}: ISaleInvoiceEditingPayload) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the invoice entries tax rate id existance when editing.
|
||||
* @param {ISaleInvoiceEditingPayload} payload -
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxIdExistanceOnEditing = async ({
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
}: ISaleInvoiceEditingPayload) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries';
|
||||
|
||||
@Service()
|
||||
export class WriteInvoiceTaxTransactionsSubscriber {
|
||||
@Inject()
|
||||
private writeTaxTransactions: WriteTaxTransactionsItemEntries;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreated,
|
||||
this.writeInvoiceTaxTransactionsOnCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEdited,
|
||||
this.rewriteInvoiceTaxTransactionsOnEdited
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDelete,
|
||||
this.removeInvoiceTaxTransactionsOnDeleted
|
||||
);
|
||||
return bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the invoice tax transactions on invoice created.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private writeInvoiceTaxTransactionsOnCreated = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx
|
||||
}: ISaleInvoiceCreatedPayload) => {
|
||||
await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
saleInvoice.entries,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the invoice tax transactions on invoice edited.
|
||||
* @param {ISaleInvoiceEditedPayload} payload -
|
||||
*/
|
||||
private rewriteInvoiceTaxTransactionsOnEdited = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) => {
|
||||
await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
saleInvoice.entries,
|
||||
'SaleInvoice',
|
||||
saleInvoice.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the invoice tax transactions on invoice deleted.
|
||||
* @param {ISaleInvoiceEditingPayload}
|
||||
*/
|
||||
private removeInvoiceTaxTransactionsOnDeleted = async ({
|
||||
tenantId,
|
||||
oldSaleInvoice,
|
||||
trx
|
||||
}: ISaleInvoiceDeletedPayload) => {
|
||||
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
oldSaleInvoice.id,
|
||||
'SaleInvoice',
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
|
||||
resetPassword: 'onResetPassword',
|
||||
resetingPassword: 'onResetingPassword'
|
||||
resetingPassword: 'onResetingPassword',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -560,4 +560,21 @@ export default {
|
||||
onDeleting: 'onProjectTimeDeleting',
|
||||
onDeleted: 'onProjectTimeDeleted',
|
||||
},
|
||||
|
||||
taxRates: {
|
||||
onCreating: 'onTaxRateCreating',
|
||||
onCreated: 'onTaxRateCreated',
|
||||
|
||||
onEditing: 'onTaxRateEditing',
|
||||
onEdited: 'onTaxRateEdited',
|
||||
|
||||
onDeleting: 'onTaxRateDeleting',
|
||||
onDeleted: 'onTaxRateDeleted',
|
||||
|
||||
onActivating: 'onTaxRateActivating',
|
||||
onActivated: 'onTaxRateActivated',
|
||||
|
||||
onInactivating: 'onTaxRateInactivating',
|
||||
onInactivated: 'onTaxRateInactivated'
|
||||
},
|
||||
};
|
||||
|
||||
@@ -471,6 +471,15 @@ const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
|
||||
return envVar ? envVar?.split(',')?.map(_.trim) : [];
|
||||
};
|
||||
|
||||
export const sortObjectKeysAlphabetically = (object) => {
|
||||
return Object.keys(object)
|
||||
.sort()
|
||||
.reduce((objEntries, key) => {
|
||||
objEntries[key] = object[key];
|
||||
return objEntries;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export {
|
||||
templateRender,
|
||||
accumSum,
|
||||
@@ -503,5 +512,5 @@ export {
|
||||
mergeObjectsBykey,
|
||||
nestedArrayToFlatten,
|
||||
assocDepthLevelToObjectTree,
|
||||
castCommaListEnvVarToArray
|
||||
castCommaListEnvVarToArray,
|
||||
};
|
||||
|
||||
19
packages/server/src/utils/taxRate.ts
Normal file
19
packages/server/src/utils/taxRate.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Get inclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getInclusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / (100 + taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get exclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getExlusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / 100;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
@bigcapitalhq:registry=https://npm.pkg.github.com
|
||||
17754
packages/webapp/package-lock.json
generated
17754
packages/webapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "@bigcapital/webapp",
|
||||
"version": "0.9.6",
|
||||
"version": "0.10.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@blueprintjs-formik/core": "^0.3.4",
|
||||
"@blueprintjs-formik/datetime": "^0.3.4",
|
||||
"@blueprintjs-formik/select": "^0.3.1",
|
||||
"@blueprintjs/core": "^3.50.2",
|
||||
"@blueprintjs/datetime": "^3.23.12",
|
||||
"@blueprintjs-formik/core": "^0.3.6",
|
||||
"@blueprintjs-formik/datetime": "^0.3.7",
|
||||
"@blueprintjs-formik/select": "^0.3.5",
|
||||
"@blueprintjs/colors": "4.1.19",
|
||||
"@blueprintjs/core": "^4.20.2",
|
||||
"@blueprintjs/datetime": "^4.4.37",
|
||||
"@blueprintjs/popover2": "^0.11.1",
|
||||
"@blueprintjs/select": "^3.11.2",
|
||||
"@blueprintjs/table": "^3.8.3",
|
||||
"@blueprintjs/timezone": "^3.6.2",
|
||||
"@blueprintjs/select": "^4.9.24",
|
||||
"@blueprintjs/table": "^4.10.12",
|
||||
"@blueprintjs/timezone": "^4.5.43",
|
||||
"@casl/ability": "^5.4.3",
|
||||
"@casl/react": "^2.3.0",
|
||||
"@craco/craco": "^5.9.0",
|
||||
@@ -26,6 +27,7 @@
|
||||
"@types/ramda": "^0.28.14",
|
||||
"@types/react": "^16.14.28",
|
||||
"@types/react-body-classname": "^1.1.7",
|
||||
"@types/react-dom": "^16.9.16",
|
||||
"@types/react-redux": "^7.1.24",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.5",
|
||||
@@ -38,44 +40,49 @@
|
||||
"axios": "^0.21.2",
|
||||
"basscss": "^8.0.2",
|
||||
"camelcase": "^5.3.1",
|
||||
"classnames": "^2.3.2",
|
||||
"cross-env": "^7.0.2",
|
||||
"deep-map-keys": "^2.0.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"dependency-graph": "^0.11.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.33.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"flat": "^5.0.2",
|
||||
"formik": "^2.2.5",
|
||||
"history": "4.10.1",
|
||||
"http-proxy-middleware": "^1.0.0",
|
||||
"jest": "24.9.0",
|
||||
"jest-environment-jsdom-fourteen": "1.0.1",
|
||||
"jest-resolve": "24.9.0",
|
||||
"jest-watch-typeahead": "0.4.2",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-money": "^0.6.3",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"node-sass": "^4.14.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "^7.1.1",
|
||||
"ramda": "^0.27.1",
|
||||
"react": "^16.14.0",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-body-classname": "^1.3.1",
|
||||
"react-content-loader": "^6.0.1",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^11.0.1",
|
||||
"react-error-boundary": "^3.0.2",
|
||||
"react-error-overlay": "^6.0.9",
|
||||
"react-hotkeys-hook": "^3.0.3",
|
||||
"react-intl-universal": "^2.4.7",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-query": "^3.6.0",
|
||||
"react-query-devtools": "^2.1.1",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-router": "5.3.4",
|
||||
"react-router-breadcrumbs-hoc": "^3.2.10",
|
||||
"react-router-dom": "^5.3.3",
|
||||
"react-scripts": "^3.4.4",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-scroll-sync": "^0.7.1",
|
||||
"react-scrollbars-custom": "^4.0.21",
|
||||
"react-sortablejs": "^2.0.11",
|
||||
@@ -86,11 +93,13 @@
|
||||
"react-use": "^13.26.1",
|
||||
"react-use-context-menu": "^0.1.4",
|
||||
"react-virtualized": "^9.22.3",
|
||||
"redux": "^4.0.5",
|
||||
"redux": "^4.2.1",
|
||||
"redux-devtools": "^3.5.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"reselect": "4.1.7",
|
||||
"rtl-detect": "^1.0.3",
|
||||
"sass": "^1.68.0",
|
||||
"semver": "6.3.0",
|
||||
"style-loader": "0.23.1",
|
||||
"styled-components": "^5.3.1",
|
||||
@@ -105,10 +114,6 @@
|
||||
"storybook": "start-storybook -p 6006"
|
||||
},
|
||||
"proxy": "http://localhost:3000/",
|
||||
"devDependencies": {
|
||||
"@types/react-dom": "^16.9.16",
|
||||
"react-error-overlay": "^6.0.9"
|
||||
},
|
||||
"resolutions": {
|
||||
"react-error-overlay": "6.0.9"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { FSelect } from '@/components/Forms';
|
||||
|
||||
export function AccountsTypesSelect({ ...props }) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
|
||||
// Filters accounts items.
|
||||
export const accountPredicate = (query, account, _index, exactMatch) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Formik, FastField, FieldArray, useFormikContext } from 'formik';
|
||||
import { Formik, FastField, FieldArray } from 'formik';
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import { Router, Switch, Route } from 'react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
|
||||
@@ -18,10 +18,10 @@ export const CardFooterActions = styled.div`
|
||||
border-top: 1px solid #e0e7ea;
|
||||
margin-top: 30px;
|
||||
|
||||
.bp3-button {
|
||||
.bp4-button {
|
||||
min-width: 70px;
|
||||
|
||||
+ .bp3-button {
|
||||
+ .bp4-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
.menu{
|
||||
:global .bp3-heading{
|
||||
:global .bp4-heading{
|
||||
font-weight: 400;
|
||||
opacity: 0.5;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import * as R from 'ramda';
|
||||
import BigcapitalLoading from './BigcapitalLoading';
|
||||
import withDashboard from '@/containers/Dashboard/withDashboard';
|
||||
|
||||
@@ -21,7 +21,7 @@ export function DataTableEditable({
|
||||
}
|
||||
|
||||
const DatatableEditableRoot = styled.div`
|
||||
.bp3-form-group {
|
||||
.bp4-form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table {
|
||||
@@ -69,17 +69,17 @@ const DatatableEditableRoot = styled.div`
|
||||
}
|
||||
.tr {
|
||||
&:hover .td,
|
||||
.bp3-input {
|
||||
.bp4-input {
|
||||
background-color: transparent;
|
||||
}
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input,
|
||||
.form-group--select-list .bp3-button {
|
||||
.bp4-form-group:not(.bp4-intent-danger) .bp4-input,
|
||||
.form-group--select-list .bp4-button {
|
||||
border-color: #ffffff;
|
||||
color: #222;
|
||||
border-radius: 3px;
|
||||
text-align: inherit;
|
||||
}
|
||||
.bp3-form-group:not(.bp3-intent-danger) .bp3-input {
|
||||
.bp4-form-group:not(.bp4-intent-danger) .bp4-input {
|
||||
border-radius: 2px;
|
||||
padding-left: 14px;
|
||||
padding-right: 14px;
|
||||
@@ -88,7 +88,7 @@ const DatatableEditableRoot = styled.div`
|
||||
box-shadow: 0 0 0 2px #116cd0;
|
||||
}
|
||||
}
|
||||
.form-group--select-list .bp3-button {
|
||||
.form-group--select-list .bp4-button {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
|
||||
@@ -97,16 +97,16 @@ const DatatableEditableRoot = styled.div`
|
||||
}
|
||||
}
|
||||
.form-group--select-list,
|
||||
.bp3-form-group {
|
||||
&.bp3-intent-danger {
|
||||
.bp3-button:not(.bp3-minimal),
|
||||
.bp3-input {
|
||||
.bp4-form-group {
|
||||
&.bp4-intent-danger {
|
||||
.bp4-button:not(.bp4-minimal),
|
||||
.bp4-input {
|
||||
border-color: #f7b6b6;
|
||||
}
|
||||
}
|
||||
}
|
||||
.td.actions {
|
||||
.bp3-button {
|
||||
.bp4-button {
|
||||
color: #80858f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const DialogFooterActionsRoot = styled.div`
|
||||
${(props) =>
|
||||
props.alignment === 'right' ? 'margin-left: auto;' : 'margin-right: auto;'};
|
||||
|
||||
.bp3-button {
|
||||
.bp4-button {
|
||||
margin-left: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import React from 'react';
|
||||
|
||||
import AccountDialog from '@/containers/Dialogs/AccountDialog';
|
||||
import InviteUserDialog from '@/containers/Dialogs/InviteUserDialog';
|
||||
import UserFormDialog from '@/containers/Dialogs/UserFormDialog';
|
||||
@@ -47,6 +45,7 @@ import ProjectExpenseForm from '@/containers/Projects/containers/ProjectExpenseF
|
||||
import EstimatedExpenseFormDialog from '@/containers/Projects/containers/EstimatedExpenseFormDialog';
|
||||
import ProjectInvoicingFormDialog from '@/containers/Projects/containers/ProjectInvoicingFormDialog';
|
||||
import ProjectBillableEntriesFormDialog from '@/containers/Projects/containers/ProjectBillableEntriesFormDialog';
|
||||
import TaxRateFormDialog from '@/containers/TaxRates/dialogs/TaxRateFormDialog/TaxRateFormDialog';
|
||||
import { DialogsName } from '@/constants/dialogs';
|
||||
|
||||
/**
|
||||
@@ -134,7 +133,10 @@ export default function DialogsContainer() {
|
||||
<ProjectInvoicingFormDialog
|
||||
dialogName={DialogsName.ProjectInvoicingForm}
|
||||
/>
|
||||
<ProjectBillableEntriesFormDialog dialogName={DialogsName.ProjectBillableEntriesForm}/>
|
||||
<ProjectBillableEntriesFormDialog
|
||||
dialogName={DialogsName.ProjectBillableEntriesForm}
|
||||
/>
|
||||
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ export function DrawerMainTabs({ children, ...restProps }) {
|
||||
}
|
||||
|
||||
const DrawerMainTabsRoot = styled.div`
|
||||
.bp3-tabs {
|
||||
.bp3-tab-list {
|
||||
.bp4-tabs {
|
||||
.bp4-tab-list {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
padding: 0 15px;
|
||||
@@ -28,7 +28,7 @@ const DrawerMainTabsRoot = styled.div`
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
&.bp3-large > .bp3-tab {
|
||||
&.bp4-large > .bp4-tab {
|
||||
font-size: 15px;
|
||||
color: #7f8596;
|
||||
margin: 0 1rem;
|
||||
@@ -38,13 +38,13 @@ const DrawerMainTabsRoot = styled.div`
|
||||
color: #0052cc;
|
||||
}
|
||||
}
|
||||
.bp3-tab-indicator-wrapper .bp3-tab-indicator {
|
||||
.bp4-tab-indicator-wrapper .bp4-tab-indicator {
|
||||
height: 2px;
|
||||
bottom: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-tab-panel {
|
||||
.bp4-tab-panel {
|
||||
margin-top: 0;
|
||||
|
||||
.card {
|
||||
|
||||
@@ -22,6 +22,7 @@ import VendorCreditDetailDrawer from '@/containers/Drawers/VendorCreditDetailDra
|
||||
import RefundCreditNoteDetailDrawer from '@/containers/Drawers/RefundCreditNoteDetailDrawer';
|
||||
import RefundVendorCreditDetailDrawer from '@/containers/Drawers/RefundVendorCreditDetailDrawer';
|
||||
import WarehouseTransferDetailDrawer from '@/containers/Drawers/WarehouseTransferDetailDrawer';
|
||||
import TaxRateDetailsDrawer from '@/containers/TaxRates/drawers/TaxRateDetailsDrawer/TaxRateDetailsDrawer';
|
||||
|
||||
import { DRAWERS } from '@/constants/drawers';
|
||||
|
||||
@@ -43,16 +44,25 @@ export default function DrawersContainer() {
|
||||
<ItemDetailDrawer name={DRAWERS.ITEM_DETAILS} />
|
||||
<CustomerDetailsDrawer name={DRAWERS.CUSTOMER_DETAILS} />
|
||||
<VendorDetailsDrawer name={DRAWERS.VENDOR_DETAILS} />
|
||||
<InventoryAdjustmentDetailDrawer name={DRAWERS.INVENTORY_ADJUSTMENT_DETAILS} />
|
||||
<CashflowTransactionDetailDrawer name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS} />
|
||||
<InventoryAdjustmentDetailDrawer
|
||||
name={DRAWERS.INVENTORY_ADJUSTMENT_DETAILS}
|
||||
/>
|
||||
<CashflowTransactionDetailDrawer
|
||||
name={DRAWERS.CASHFLOW_TRNASACTION_DETAILS}
|
||||
/>
|
||||
<QuickCreateCustomerDrawer name={DRAWERS.QUICK_CREATE_CUSTOMER} />
|
||||
<QuickCreateItemDrawer name={DRAWERS.QUICK_CREATE_ITEM} />
|
||||
<QuickWriteVendorDrawer name={DRAWERS.QUICK_WRITE_VENDOR} />
|
||||
<CreditNoteDetailDrawer name={DRAWERS.CREDIT_NOTE_DETAILS} />
|
||||
<VendorCreditDetailDrawer name={DRAWERS.VENDOR_CREDIT_DETAILS} />
|
||||
<RefundCreditNoteDetailDrawer name={DRAWERS.REFUND_CREDIT_NOTE_DETAILS} />
|
||||
<RefundVendorCreditDetailDrawer name={DRAWERS.REFUND_VENDOR_CREDIT_DETAILS} />
|
||||
<WarehouseTransferDetailDrawer name={DRAWERS.WAREHOUSE_TRANSFER_DETAILS} />
|
||||
<RefundVendorCreditDetailDrawer
|
||||
name={DRAWERS.REFUND_VENDOR_CREDIT_DETAILS}
|
||||
/>
|
||||
<WarehouseTransferDetailDrawer
|
||||
name={DRAWERS.WAREHOUSE_TRANSFER_DETAILS}
|
||||
/>
|
||||
<TaxRateDetailsDrawer name={DRAWERS.TAX_RATE_DETAILS} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,8 +60,8 @@ export function ExchangeRateMutedField({
|
||||
}
|
||||
|
||||
const ExchangeRateFormGroup = styled(FormGroup)`
|
||||
&.bp3-form-group {
|
||||
label.bp3-label {
|
||||
&.bp4-form-group {
|
||||
label.bp4-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
line-height: 1;
|
||||
@@ -79,7 +79,7 @@ const ExchangeRateButton = styled.div`
|
||||
position: relative;
|
||||
padding-right: 28px;
|
||||
|
||||
.bp3-button {
|
||||
.bp4-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const ExchangeRateButton = styled.div`
|
||||
const ExchangeRateFormGroupContent = styled.div`
|
||||
padding: 5px 0;
|
||||
|
||||
.bp3-form-group {
|
||||
.bp4-form-group {
|
||||
padding: 2px;
|
||||
margin: 2px 4px !important;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
EditableText,
|
||||
TextArea,
|
||||
} from '@blueprintjs-formik/core';
|
||||
import { MultiSelect } from '@blueprintjs-formik/select';
|
||||
import { MultiSelect, SuggestField } from '@blueprintjs-formik/select';
|
||||
import { DateInput } from '@blueprintjs-formik/datetime';
|
||||
import { FSelect } from './Select';
|
||||
|
||||
@@ -24,6 +24,7 @@ export {
|
||||
FSelect,
|
||||
MultiSelect as FMultiSelect,
|
||||
EditableText as FEditableText,
|
||||
SuggestField as FSuggest,
|
||||
TextArea as FTextArea,
|
||||
DateInput as FDateInput,
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ const SelectButton = styled(Button)`
|
||||
position: relative;
|
||||
padding-right: 30px;
|
||||
|
||||
&:not(.is-selected):not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||
&:not(.is-selected):not([class*='bp4-intent-']):not(.bp4-minimal) {
|
||||
color: #5c7080;
|
||||
}
|
||||
&:after {
|
||||
@@ -46,13 +46,13 @@ const SelectButton = styled(Button)`
|
||||
margin-right: 12px;
|
||||
border-radius: 1px;
|
||||
}
|
||||
&:not([class*='bp3-intent-']):not(.bp3-disabled) {
|
||||
&:not([class*='bp4-intent-']):not(.bp4-disabled) {
|
||||
&,
|
||||
&:hover {
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
.bp3-intent-danger & {
|
||||
.bp4-intent-danger & {
|
||||
border-color: #db3737;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export function MenuItemLabel({ text }) {
|
||||
return <span class="bp3-menu-item-labeler">{text}</span>;
|
||||
return <span class="bp4-menu-item-labeler">{text}</span>;
|
||||
}
|
||||
|
||||
@@ -18,16 +18,16 @@ const FormTopBarRoot = styled(Navbar)`
|
||||
height: 35px;
|
||||
padding: 0 20px;
|
||||
|
||||
.bp3-navbar-group {
|
||||
.bp4-navbar-group {
|
||||
height: 35px;
|
||||
}
|
||||
.bp3-navbar-divider {
|
||||
.bp4-navbar-divider {
|
||||
border-left-color: #d2dce2;
|
||||
}
|
||||
.bp3-skeleton {
|
||||
.bp4-skeleton {
|
||||
max-height: 10px;
|
||||
}
|
||||
.bp3-button {
|
||||
.bp4-button {
|
||||
&:hover {
|
||||
background: rgba(167, 182, 194, 0.12);
|
||||
color: #32304a;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-nocheck
|
||||
import React, { useCallback } from 'react';
|
||||
import { Suggest } from '@blueprintjs-formik/select';
|
||||
import { FormGroup } from '@blueprintjs/core';
|
||||
import { CellType } from '@/constants';
|
||||
|
||||
export function TaxRatesSuggestInputCell({
|
||||
column: { id, suggestProps, formGroupProps },
|
||||
row: { index },
|
||||
cell: { value: cellValue },
|
||||
payload: { errors, updateData, taxRates },
|
||||
}) {
|
||||
const error = errors?.[index]?.[id];
|
||||
|
||||
// Handle the item selected.
|
||||
const handleItemSelected = useCallback(
|
||||
(value, taxRate) => {
|
||||
updateData(index, id, taxRate.id);
|
||||
},
|
||||
[updateData, index, id],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup intent={error ? Intent.DANGER : null} {...formGroupProps}>
|
||||
<Suggest<any>
|
||||
selectedValue={cellValue}
|
||||
items={taxRates}
|
||||
valueAccessor={'id'}
|
||||
labelAccessor={'code'}
|
||||
textAccessor={'name_formatted'}
|
||||
popoverProps={{ minimal: true, boundary: 'window' }}
|
||||
inputProps={{ placeholder: '' }}
|
||||
fill={true}
|
||||
onItemChange={handleItemSelected}
|
||||
{...suggestProps}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
TaxRatesSuggestInputCell.cellType = CellType.Field;
|
||||
@@ -1,5 +1,4 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const For = ({ render, of }) =>
|
||||
|
||||
@@ -20,7 +20,8 @@ export const AbilitySubject = {
|
||||
SubscriptionBilling: 'SubscriptionBilling',
|
||||
CreditNote: 'CreditNote',
|
||||
VendorCredit: 'VendorCredit',
|
||||
Project:'Project'
|
||||
Project:'Project',
|
||||
TaxRate: 'TaxRate',
|
||||
};
|
||||
|
||||
export const ItemAction = {
|
||||
@@ -169,6 +170,7 @@ export const ReportsAction = {
|
||||
READ_INVENTORY_VALUATION_SUMMARY: 'read-inventory-valuation-summary',
|
||||
READ_INVENTORY_ITEM_DETAILS: 'read-inventory-item-details',
|
||||
READ_CASHFLOW_ACCOUNT_TRANSACTION: 'read-cashflow-account-transactions',
|
||||
READ_SALES_TAX_LIABILITY_SUMMARY: 'read-sales-tax-liability-summary',
|
||||
};
|
||||
|
||||
export const PreferencesAbility = {
|
||||
@@ -185,3 +187,11 @@ export const SubscriptionBillingAbility = {
|
||||
View: 'view',
|
||||
Payment: 'payment',
|
||||
};
|
||||
|
||||
|
||||
export const TaxRateAction = {
|
||||
View: 'View',
|
||||
Create: 'Create',
|
||||
Edit: 'Edit',
|
||||
Delete: 'Delete',
|
||||
};
|
||||
|
||||
@@ -46,5 +46,6 @@ export enum DialogsName {
|
||||
EstimateExpenseForm = 'estimate-expense-form',
|
||||
ProjectInvoicingForm = 'project-invoicing-form',
|
||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings'
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||
TaxRateForm = 'tax-rate-form',
|
||||
}
|
||||
|
||||
@@ -22,4 +22,5 @@ export enum DRAWERS {
|
||||
REFUND_CREDIT_NOTE_DETAILS = 'refund-credit-detail-drawer',
|
||||
REFUND_VENDOR_CREDIT_DETAILS = 'refund-vendor-detail-drawer',
|
||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||
}
|
||||
|
||||
@@ -87,9 +87,6 @@ export const financialReportMenus = [
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const SalesAndPurchasesReportMenus = [
|
||||
{
|
||||
sectionTitle: <T id={'sales_purchases_reports'} />,
|
||||
reports: [
|
||||
@@ -119,19 +116,6 @@ export const SalesAndPurchasesReportMenus = [
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_BY_ITEMS,
|
||||
},
|
||||
{
|
||||
title: <T id={'inventory_valuation'} />,
|
||||
desc: (
|
||||
<T
|
||||
id={
|
||||
'summarize_the_business_s_purchase_items_quantity_cost_and_average'
|
||||
}
|
||||
/>
|
||||
),
|
||||
link: '/financial-reports/inventory-valuation',
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
|
||||
},
|
||||
{
|
||||
title: <T id={'customers_balance_summary'} />,
|
||||
desc: (
|
||||
@@ -189,4 +173,16 @@ export const SalesAndPurchasesReportMenus = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionTitle: 'Taxes',
|
||||
reports: [
|
||||
{
|
||||
title: 'Sales Tax Liability Summary',
|
||||
desc: 'Reports the total amount of sales tax collected from customers',
|
||||
link: '/financial-reports/sales-tax-liability-summary',
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ExpenseAction,
|
||||
CashflowAction,
|
||||
PreferencesAbility,
|
||||
TaxRateAction,
|
||||
} from '@/constants/abilityOption';
|
||||
import { DialogsName } from './dialogs';
|
||||
|
||||
@@ -406,6 +407,15 @@ export const SidebarMenu = [
|
||||
href: '/transactions-locking',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
},
|
||||
{
|
||||
text: 'Tax Rates',
|
||||
href: '/tax-rates',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
permission: {
|
||||
subject: AbilitySubject.TaxRate,
|
||||
ability: TaxRateAction.View,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -741,6 +751,21 @@ export const SidebarMenu = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Taxes',
|
||||
type: ISidebarMenuItemType.Group,
|
||||
children: [
|
||||
{
|
||||
text: 'Sales Tax Liability Summary',
|
||||
href: '/financial-reports/sales-tax-liability-summary',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
permission: {
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: <T id={'sidebar.inventory'} />,
|
||||
type: ISidebarMenuItemType.Group,
|
||||
|
||||
@@ -22,12 +22,12 @@ export function MakeJournalFormFooterLeft() {
|
||||
}
|
||||
|
||||
const DescriptionFormGroup = styled(FFormGroup)`
|
||||
&.bp3-form-group {
|
||||
.bp3-label {
|
||||
&.bp4-form-group {
|
||||
.bp4-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.bp3-form-content {
|
||||
.bp4-form-content {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user