Compare commits

..

63 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
04b16a7d2f Publish
- @bigcapital/server@0.10.1
 - @bigcapital/webapp@0.10.2
2023-10-03 22:07:42 +02:00
Ahmed Bouhuolia
fd27136274 tst 2023-10-03 22:06:33 +02:00
Ahmed Bouhuolia
951e360405 chore(webapp): add missing packages 2023-10-03 22:05:57 +02:00
Ahmed Bouhuolia
d2242b7744 WIP 2023-10-03 12:11:55 +02:00
Ahmed Bouhuolia
4e9b0ae24f fix: migrate from npm to pnpm 2023-09-29 15:54:47 +02:00
Ahmed Bouhuolia
e5ffb5661a feat: migrate to pnpm 2023-09-27 22:44:54 +02:00
Ahmed Bouhuolia
9eaff0785b chore: update CHANGELOG.md file 2023-09-25 17:14:07 +02:00
Ahmed Bouhuolia
b3a97ed5d5 chore: dump packages versions 2023-09-25 15:34:10 +02:00
Ahmed Bouhuolia
1aaa65edd2 Merge pull request #242 from bigcapitalhq/abouhuolia/big-62-running-tenants-dbs-migration-on-migration-docker-container
fix: Running tenants migration on docker migration container
2023-09-25 15:31:54 +02:00
Ahmed Bouhuolia
efebf424d1 fix: running tenants migration on docker migration container 2023-09-25 15:31:09 +02:00
Ahmed Bouhuolia
597973d5d1 chore: update @blueprintjs-formik/select package. 2023-09-24 12:30:01 +02:00
Ahmed Bouhuolia
ee6bc822c9 Merge pull request #240 from bigcapitalhq/BIG-61-make-tables-header-sticy
fix: Table headers sticky for all reports.
2023-09-24 12:15:51 +02:00
ElforJani13
07e78ebd6a Sticky table header for all reports 2023-09-24 04:58:16 +02:00
Ahmed Bouhuolia
8c04a6205b Merge pull request #197 from bigcapitalhq/dependabot/npm_and_yarn/mongoose-5.13.20
chore(deps): bump mongoose from 5.13.15 to 5.13.20
2023-09-23 14:43:25 +02:00
Ahmed Bouhuolia
d54ea68d46 Merge pull request #199 from bigcapitalhq/dependabot/npm_and_yarn/packages/webapp/word-wrap-1.2.4
chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp
2023-09-23 14:43:12 +02:00
Ahmed Bouhuolia
f42c8031c2 Merge pull request #200 from bigcapitalhq/dependabot/npm_and_yarn/word-wrap-1.2.4
chore(deps): bump word-wrap from 1.2.3 to 1.2.4
2023-09-23 14:42:49 +02:00
Ahmed Bouhuolia
5261d332b7 Merge pull request #204 from bigcapitalhq/tax-compliance
feat: [wip] tax rates service
2023-09-23 14:41:32 +02:00
Ahmed Bouhuolia
f1e7d0fc44 fix(server): sales tax liability summary report 2023-09-23 14:40:21 +02:00
Ahmed Bouhuolia
1d1049043e fix(webapp): invoice total currency should be dynamic 2023-09-23 14:19:59 +02:00
Ahmed Bouhuolia
1148fef9ad fix(server): tax tracking on sale invoices 2023-09-23 14:19:42 +02:00
Ahmed Bouhuolia
92ac0dbd25 fix(webapp): code cleanup 2023-09-22 15:25:56 +02:00
Ahmed Bouhuolia
ce41845bd7 fix(server): pull-request nodes 2023-09-22 15:23:33 +02:00
Ahmed Bouhuolia
eaf72d1608 fix(server): tax percentage calculation of tax sales liability summary report 2023-09-20 19:25:37 +02:00
Ahmed Bouhuolia
ac336f9878 feat(webapp): add tax compund tag to tax rates 2023-09-20 19:25:07 +02:00
Ahmed Bouhuolia
746c80c564 fix(server): tax rate could be zero. 2023-09-20 17:22:58 +02:00
Ahmed Bouhuolia
601434b107 feat: avoid invoice writes GL entry with zero amount 2023-09-20 17:22:39 +02:00
Ahmed Bouhuolia
5aaa33e585 feat(webapp): add activate/inactivate tax rate buttons on details drawer 2023-09-20 15:06:27 +02:00
Ahmed Bouhuolia
d48d864a5f fix(sever): tax rates cell. 2023-09-20 00:43:55 +02:00
Ahmed Bouhuolia
453df2ac4e fix(server): Validation of activate/inacitvate tax rates 2023-09-20 00:42:34 +02:00
Ahmed Bouhuolia
73ceeaee46 fix(server): [Sales Tax Liability Report] filter non-transactions tax rates 2023-09-20 00:42:03 +02:00
Ahmed Bouhuolia
1b4b656419 feat(server): order tax rates by name 2023-09-18 18:57:54 +02:00
Ahmed Bouhuolia
df823c0bfe feat(webapp): tax rates empty state 2023-09-18 18:57:24 +02:00
Ahmed Bouhuolia
e91d7b0cff feat(webapp): tax rate form validation errors 2023-09-18 11:23:50 +02:00
Ahmed Bouhuolia
aa52e7d02c feat(server): soft deleting tax rates 2023-09-18 10:15:55 +02:00
Ahmed Bouhuolia
4e53d08497 feat(server): wip activate/inactivate tax rate 2023-09-18 01:38:38 +02:00
Ahmed Bouhuolia
2356921f27 feat(webapp): wip tax rate form dialog 2023-09-18 01:35:53 +02:00
Ahmed Bouhuolia
a0a5d00be3 chore: Add Patreon link for funding 2023-09-15 01:34:45 +02:00
Ahmed Bouhuolia
fbd74c559b feat(server): tweak the tax rate transformer 2023-09-14 23:36:15 +02:00
Ahmed Bouhuolia
8a64198433 feat(webapp): wip tax rates management 2023-09-14 23:35:54 +02:00
Ahmed Bouhuolia
b98b73ad98 feat(webapp): invoice tax rate 2023-09-11 23:17:27 +02:00
Ahmed Bouhuolia
6abae43c6f feat: tax rate transformer 2023-09-11 20:46:46 +02:00
Ahmed Bouhuolia
7657337c4f feat: sales tax report query 2023-09-08 19:49:46 +02:00
Ahmed Bouhuolia
983ceb5cc6 feat: sale invoice model tax attributes 2023-09-06 14:01:40 +02:00
Ahmed Bouhuolia
ac072d29fc feat(server): wip sale invoice tax rate GL entries 2023-09-04 18:39:49 +02:00
Ahmed Bouhuolia
17e055db5e chore: update README.md file 2023-09-03 01:35:10 +02:00
Ahmed Bouhuolia
b49a021506 feat: contact us section to README.md file 2023-09-03 01:32:06 +02:00
Ahmed Bouhuolia
b49b45fb43 feat: wip sales tax summry report 2023-09-02 01:52:07 +02:00
Ahmed Bouhuolia
bb7cf41e3e feat: add sales tax summary report to reports list 2023-09-02 01:51:28 +02:00
Ahmed Bouhuolia
801ea5dfdb feat: wip sales tax summary report 2023-09-02 01:50:24 +02:00
Ahmed Bouhuolia
eb03a38553 feat(webapp): wip sales tax summary report 2023-09-01 20:50:22 +02:00
Ahmed Bouhuolia
0852feecbf fix(server): avoid display total row if no tax rates on sales tax report 2023-09-01 20:48:23 +02:00
Ahmed Bouhuolia
54dcde657f feat(webapp): wip sales tax liability summary report 2023-09-01 01:39:16 +02:00
Ahmed Bouhuolia
5bb95eeb1a feat: wip sales tax liability summary report 2023-08-31 21:39:59 +02:00
Ahmed Bouhuolia
6baec8dd96 feat(server): wip sales tax liability summary report 2023-08-31 02:19:18 +02:00
Ahmed Bouhuolia
6535424d0f feat(server): wip sale invoice tax rates 2023-08-29 19:12:19 +02:00
Ahmed Bouhuolia
09d73db20f Merge branch 'develop' into tax-compliance 2023-08-29 15:09:52 +02:00
Ahmed Bouhuolia
d1121f0b81 feat(server): wip tax rate on sale invoice service 2023-08-14 14:59:10 +02:00
Ahmed Bouhuolia
a7644e6481 feat: tax rates on sale invoice service 2023-08-11 21:08:30 +02:00
Ahmed Bouhuolia
d6f56568a3 feat: tax rates crud service 2023-08-11 16:00:39 +02:00
Ahmed Bouhuolia
04d134806b feat(server): wip tax rates service 2023-08-11 01:31:52 +02:00
dependabot[bot]
339559d830 chore(deps): bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-20 06:12:05 +00:00
dependabot[bot]
22e4d757e4 chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-20 06:08:24 +00:00
dependabot[bot]
3b98e8de80 chore(deps): bump mongoose from 5.13.15 to 5.13.20
Bumps [mongoose](https://github.com/Automattic/mongoose) from 5.13.15 to 5.13.20.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/5.13.15...5.13.20)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-18 21:00:25 +00:00
347 changed files with 32152 additions and 24753 deletions

13
.github/FUNDING.yml vendored Normal file
View 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']

View File

@@ -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)

View File

@@ -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">

View File

@@ -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

View File

@@ -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"]

View 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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -1,7 +1,6 @@
/node_modules/
/.env
/storage
package-lock.json
stdout.log
/dist
/build
/build

View File

@@ -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",

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}

View 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);
}
}

View File

@@ -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());

View File

@@ -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');
};

View File

@@ -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);
});
}
}

View File

@@ -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',
});
});
}
}

View 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,
},
];

View File

@@ -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,
}
];
},
];

View File

@@ -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',
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -48,6 +48,9 @@ export interface ILedgerEntry {
branchId?: number;
projectId?: number;
taxRateId?: number;
taxRate?: number;
entryId?: number;
createdAt?: Date;

View File

@@ -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;

View File

@@ -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 {

View 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;
}

View 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',
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
];
};

View File

@@ -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));
};

View File

@@ -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;
}
/**

View File

@@ -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',
},
},
};
}
}

View File

@@ -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');
},
},
};
}

View 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 {};
}
}

View 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',
},
},
};
}
}

View File

@@ -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 = {},

View File

@@ -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;
}

View File

@@ -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,
};
}

View File

@@ -32,5 +32,8 @@ export const transformLedgerEntryToTransaction = (
projectId: entry.projectId,
costable: entry.costable,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,
};
};

View File

@@ -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 };
};
}

View File

@@ -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');
};
}

View File

@@ -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,
};
}
}

View File

@@ -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',
},
]);
}
}

View File

@@ -0,0 +1,4 @@
export enum IROW_TYPE {
TaxRate = 'TaxRate',
Total = 'Total',
}

View File

@@ -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);
}
}

View File

@@ -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);
};
}

View File

@@ -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);

View File

@@ -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];
};
}

View File

@@ -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,
});
};
}

View File

@@ -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,
});
};
}

View File

@@ -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>}

View 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;
});
}
}

View File

@@ -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);
}
}
}

View 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;
});
}
}

View 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);
});
}
}

View 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;
});
}
}

View 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()
);
}
}

View 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()
);
}
}

View 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;
});
}
}

View File

@@ -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;
});
};
}

View 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}%]`;
};
}

View 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);
}
}

View File

@@ -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();
}
}

View 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'
};

View File

@@ -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
);
};
}

View File

@@ -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
);
};
}

View File

@@ -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'
},
};

View File

@@ -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,
};

View 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;
};

View File

@@ -1 +0,0 @@
@bigcapitalhq:registry=https://npm.pkg.github.com

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import { FSelect } from '@/components/Forms';
export function AccountsTypesSelect({ ...props }) {

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
// Filters accounts items.
export const accountPredicate = (query, account, _index, exactMatch) => {

View File

@@ -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,

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -1,6 +1,6 @@
.menu{
:global .bp3-heading{
:global .bp4-heading{
font-weight: 400;
opacity: 0.5;
font-size: 12px;

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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>
);
}

View File

@@ -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 {

View File

@@ -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>
);
}

View File

@@ -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;
}

View File

@@ -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,
};

View File

@@ -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;
}
`;

View File

@@ -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>;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,5 +1,4 @@
// @ts-nocheck
import React from 'react';
import PropTypes from 'prop-types';
export const For = ({ render, of }) =>

View File

@@ -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',
};

View File

@@ -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',
}

View File

@@ -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',
}

View File

@@ -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,
},
],
},
];

View File

@@ -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,

View File

@@ -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