Compare commits

...

56 Commits

Author SHA1 Message Date
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
7f746b96c8 chore: remove /data directory from git ignored dirs 2023-08-29 03:05:19 +02:00
Ahmed Bouhuolia
ed2bca6b74 chore: dump CHANGELOG for 0.9.12 2023-08-29 02:54:03 +02:00
Ahmed Bouhuolia
f9d5a3c69a Merge pull request #231 from bigcapitalhq/fix-filter-transactions-date-format
fix(server): date format of filtering transactions by date range
2023-08-29 02:42:35 +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
149 changed files with 6220 additions and 438 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']

7
.gitignore vendored
View File

@@ -1,4 +1,9 @@
node_modules/
data
# Docker volumes data directory
/data
# Production env file
.env
test-results/

View File

@@ -2,6 +2,20 @@
All notable changes to Bigcapital server-side will be in this file.
# [0.9.12] - 29-08-2023
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
* Fix: typo in bill success message without bill number by @KalliopiPliogka in https://github.com/bigcapitalhq/bigcapital/pull/219
* Fix: AP/AR aging summary issue by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/229
* Fix: shouldn't write GL entries when save transaction as draft. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/221
* Fix: Transaction type of credit note and vendor credit are not defined on account transactions by @abouolia in
* Fix: date format of filtering transactions by date range by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/231
* Fix: change the default from/date date value of reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/230
* Fix: typos in words start with `A` letter by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/227
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
https://github.com/bigcapitalhq/bigcapital/pull/225
# [0.9.11] - 23-07-2023
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198

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

@@ -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,28 @@
export const TransactionTypes = {
SaleInvoice: 'Sale invoice',
SaleReceipt: 'Sale receipt',
PaymentReceive: 'Payment receive',
Bill: 'Bill',
BillPayment: 'Payment made',
VendorOpeningBalance: 'Vendor opening balance',
CustomerOpeningBalance: 'Customer opening balance',
InventoryAdjustment: 'Inventory adjustment',
ManualJournal: 'Manual journal',
Journal: 'Manual journal',
Expense: 'Expense',
OwnerContribution: 'Owner contribution',
TransferToAccount: 'Transfer to account',
TransferFromAccount: 'Transfer from account',
OtherIncome: 'Other income',
OtherExpense: 'Other expense',
OwnerDrawing: 'Owner drawing',
InvoiceWriteOff: 'Invoice write-off',
CreditNote: 'transaction_type.credit_note',
VendorCredit: 'transaction_type.vendor_credit',
RefundCreditNote: 'transaction_type.refund_credit_note',
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
LandedCost: 'transaction_type.landed_cost',
};

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

@@ -1227,13 +1227,12 @@
}
},
"@blueprintjs-formik/select": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.3.1.tgz",
"integrity": "sha512-gEoXne1kOPSq8hoQmJ3OyE1HMQAFYsSKnddN59dmkWTgobKxA+hH7mcdhbbkVSx93r3wg/oyWw4CHxOaZspGOQ==",
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@blueprintjs-formik/select/-/select-0.3.2.tgz",
"integrity": "sha512-5sxC2ucx316EkkovUVoPBUblVbxtclC7//XaxPTH7ALgipD30evP6cNTE4NtCrbGN62/TjyCjoiYsAVQhc9+6w==",
"requires": {
"lodash.get": "^4.4.2",
"lodash.keyby": "^4.6.0",
"styled-components": "^5.3.3",
"web-vitals": "^2.1.4"
}
},
@@ -17445,9 +17444,9 @@
}
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA=="
},
"workbox-background-sync": {
"version": "4.3.1",

View File

@@ -5,7 +5,7 @@
"dependencies": {
"@blueprintjs-formik/core": "^0.3.4",
"@blueprintjs-formik/datetime": "^0.3.4",
"@blueprintjs-formik/select": "^0.3.1",
"@blueprintjs-formik/select": "^0.3.2",
"@blueprintjs/core": "^3.50.2",
"@blueprintjs/datetime": "^3.23.12",
"@blueprintjs/popover2": "^0.11.1",

View File

@@ -47,6 +47,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 +135,10 @@ export default function DialogsContainer() {
<ProjectInvoicingFormDialog
dialogName={DialogsName.ProjectInvoicingForm}
/>
<ProjectBillableEntriesFormDialog dialogName={DialogsName.ProjectBillableEntriesForm}/>
<ProjectBillableEntriesFormDialog
dialogName={DialogsName.ProjectBillableEntriesForm}
/>
<TaxRateFormDialog dialogName={DialogsName.TaxRateForm} />
</div>
);
}

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

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

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

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

@@ -25,6 +25,7 @@ import WarehousesAlerts from '@/containers/Preferences/Warehouses/WarehousesAler
import WarehousesTransfersAlerts from '@/containers/WarehouseTransfers/WarehousesTransfersAlerts';
import BranchesAlerts from '@/containers/Preferences/Branches/BranchesAlerts';
import ProjectAlerts from '@/containers/Projects/containers/ProjectAlerts';
import TaxRatesAlerts from '@/containers/TaxRates/alerts';
export default [
...AccountsAlerts,
@@ -53,4 +54,5 @@ export default [
...WarehousesTransfersAlerts,
...BranchesAlerts,
...ProjectAlerts,
...TaxRatesAlerts
];

View File

@@ -25,14 +25,12 @@ import { InvoiceDetailsStatus } from './utils';
export default function InvoiceDetailHeader() {
const { invoice } = useInvoiceDetailDrawerContext();
const handleCustomerLinkClick = () => {};
return (
<CommercialDocHeader>
<CommercialDocTopHeader>
<DetailsMenu>
<AmountDetailItem label={intl.get('amount')}>
<h3 class="big-number">{invoice.formatted_amount}</h3>
<h3 class="big-number">{invoice.total_formatted}</h3>
</AmountDetailItem>
<StatusDetailItem label={''}>
@@ -75,11 +73,11 @@ export default function InvoiceDetailHeader() {
textAlign={'right'}
>
<DetailItem label={intl.get('due_amount')}>
<strong>{invoice.formatted_due_amount}</strong>
<strong>{invoice.due_amount_formatted}</strong>
</DetailItem>
<DetailItem label={intl.get('invoice.details.payment_amount')}>
<strong>{invoice.formatted_payment_amount}</strong>
<strong>{invoice.payment_amount_formatted}</strong>
</DetailItem>
<DetailItem

View File

@@ -23,22 +23,30 @@ export function InvoiceDetailTableFooter() {
<InvoiceTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
<TotalLine
title={<T id={'invoice.details.subtotal'} />}
value={<FormatNumber value={invoice.balance} />}
value={<FormatNumber value={invoice.subtotal_formatted} />}
borderStyle={TotalLineBorderStyle.SingleDark}
/>
{invoice.taxes.map((taxRate) => (
<TotalLine
key={taxRate.id}
title={`${taxRate.name} [${taxRate.tax_rate}%]`}
value={taxRate.tax_rate_amount_formatted}
textStyle={TotalLineTextStyle.Regular}
/>
))}
<TotalLine
title={<T id={'invoice.details.total'} />}
value={invoice.formatted_amount}
value={invoice.total_formatted}
borderStyle={TotalLineBorderStyle.DoubleDark}
textStyle={TotalLineTextStyle.Bold}
/>
<TotalLine
title={<T id={'invoice.details.payment_amount'} />}
value={invoice.formatted_payment_amount}
value={invoice.payment_amount_formatted}
/>
<TotalLine
title={<T id={'invoice.details.due_amount'} />}
value={invoice.formatted_due_amount}
value={invoice.due_amount_formatted}
textStyle={TotalLineTextStyle.Bold}
/>
</InvoiceTotalLines>

View File

@@ -0,0 +1,20 @@
// @ts-nocheck
import React, { createContext } from 'react';
const ItemEntriesTableContext = createContext();
function ItemEntriesTableProvider({ children, value }) {
const provider = {
...value,
};
return (
<ItemEntriesTableContext.Provider value={provider}>
{children}
</ItemEntriesTableContext.Provider>
);
}
const useItemEntriesTableContext = () =>
React.useContext(ItemEntriesTableContext);
export { ItemEntriesTableProvider, useItemEntriesTableContext };

View File

@@ -1,103 +1,104 @@
// @ts-nocheck
import React, { useEffect, useCallback } from 'react';
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { CLASSES } from '@/constants/classes';
import { DataTableEditable } from '@/components';
import { useEditableItemsEntriesColumns } from './components';
import {
saveInvoke,
compose,
updateMinEntriesLines,
updateRemoveLineByIndex,
} from '@/utils';
import {
useFetchItemRow,
composeRowsOnNewRow,
composeRowsOnEditCell,
useComposeRowsOnEditTableCell,
useComposeRowsOnRemoveTableRow,
} from './utils';
import {
ItemEntriesTableProvider,
useItemEntriesTableContext,
} from './ItemEntriesTableProvider';
import { useUncontrolled } from '@/hooks/useUncontrolled';
/**
* Items entries table.
*/
function ItemsEntriesTable({
// #ownProps
items,
entries,
initialEntries,
defaultEntry,
errors,
onUpdateData,
currencyCode,
itemType, // sellable or purchasable
landedCost = false,
minLinesNumber
}) {
const [rows, setRows] = React.useState(initialEntries);
function ItemsEntriesTable(props) {
const { value, initialValue, onChange } = props;
// Allows to observes `entries` to make table rows outside controlled.
useEffect(() => {
if (entries && entries !== rows) {
setRows(entries);
}
}, [entries, rows]);
const [localValue, handleChange] = useUncontrolled({
value,
initialValue,
finalValue: [],
onChange,
});
return (
<ItemEntriesTableProvider value={{ ...props, localValue, handleChange }}>
<ItemEntriesTableRoot />
</ItemEntriesTableProvider>
);
}
/**
* Items entries table logic.
* @returns {JSX.Element}
*/
function ItemEntriesTableRoot() {
const {
localValue,
defaultEntry,
handleChange,
items,
errors,
currencyCode,
landedCost,
taxRates,
} = useItemEntriesTableContext();
// Editiable items entries columns.
const columns = useEditableItemsEntriesColumns({ landedCost });
const columns = useEditableItemsEntriesColumns();
const composeRowsOnEditCell = useComposeRowsOnEditTableCell();
const composeRowsOnDeleteRow = useComposeRowsOnRemoveTableRow();
// Handle the fetch item row details.
const { setItemRow, cellsLoading, isItemFetching } = useFetchItemRow({
landedCost,
itemType,
itemType: null,
notifyNewRow: (newRow, rowIndex) => {
// Update the rate, description and quantity data of the row.
const newRows = composeRowsOnNewRow(rowIndex, newRow, rows);
setRows(newRows);
onUpdateData(newRows);
const newRows = composeRowsOnNewRow(rowIndex, newRow, localValue);
handleChange(newRows);
},
});
// Handles the editor data update.
const handleUpdateData = useCallback(
(rowIndex, columnId, value) => {
if (columnId === 'item_id') {
setItemRow({ rowIndex, columnId, itemId: value });
}
const composeEditCell = composeRowsOnEditCell(rowIndex, columnId);
const newRows = composeEditCell(value, defaultEntry, rows);
setRows(newRows);
onUpdateData(newRows);
const newRows = composeRowsOnEditCell(rowIndex, columnId, value);
handleChange(newRows);
},
[rows, defaultEntry, onUpdateData, setItemRow],
[localValue, defaultEntry, handleChange],
);
// Handle table rows removing by index.
const handleRemoveRow = (rowIndex) => {
const newRows = compose(
// Ensure minimum lines count.
updateMinEntriesLines(minLinesNumber, defaultEntry),
// Remove the line by the given index.
updateRemoveLineByIndex(rowIndex),
)(rows);
setRows(newRows);
saveInvoke(onUpdateData, newRows);
const newRows = composeRowsOnDeleteRow(rowIndex);
handleChange(newRows);
};
return (
<DataTableEditable
className={classNames(CLASSES.DATATABLE_EDITOR_ITEMS_ENTRIES)}
columns={columns}
data={rows}
data={localValue}
sticky={true}
progressBarLoading={isItemFetching}
cellsLoading={isItemFetching}
cellsLoadingCoords={cellsLoading}
payload={{
items,
taxRates,
errors: errors || [],
updateData: handleUpdateData,
removeRow: handleRemoveRow,

View File

@@ -17,6 +17,8 @@ import {
ProjectBillableEntriesCell,
} from '@/components/DataTableCells';
import { useFeatureCan } from '@/hooks/state';
import { TaxRatesSuggestInputCell } from '@/components/TaxRates/TaxRatesSuggestInputCell';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
/**
* Item header cell.
@@ -43,7 +45,6 @@ export function ActionsCellRenderer({
const onRemoveRole = () => {
removeRow(index);
};
const exampleMenu = (
<Menu>
<MenuItem
@@ -89,15 +90,17 @@ const LandedCostHeaderCell = () => {
/**
* Retrieve editable items entries columns.
*/
export function useEditableItemsEntriesColumns({ landedCost }) {
export function useEditableItemsEntriesColumns() {
const { featureCan } = useFeatureCan();
const { landedCost } = useItemEntriesTableContext();
const isProjectsFeatureEnabled = featureCan(Features.Projects);
return React.useMemo(
() => [
{
Header: ItemHeaderCell,
id: 'item_id',
Header: ItemHeaderCell,
accessor: 'item_id',
Cell: ItemsListCell,
disableSortBy: true,
@@ -129,6 +132,13 @@ export function useEditableItemsEntriesColumns({ landedCost }) {
width: 70,
align: Align.Right,
},
{
Header: 'Tax rate',
accessor: 'tax_rate_id',
Cell: TaxRatesSuggestInputCell,
disableSortBy: true,
width: 110,
},
{
Header: intl.get('discount'),
accessor: 'discount',

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import React, { useCallback } from 'react';
import * as R from 'ramda';
import { sumBy, isEmpty, last } from 'lodash';
import { sumBy, isEmpty, last, keyBy } from 'lodash';
import { useItem } from '@/hooks/query';
import {
@@ -13,6 +13,12 @@ import {
orderingLinesIndexes,
updateTableRow,
} from '@/utils';
import { useItemEntriesTableContext } from './ItemEntriesTableProvider';
export const ITEM_TYPE = {
SELLABLE: 'SELLABLE',
PURCHASABLE: 'PURCHASABLE',
};
/**
* Retrieve item entry total from the given rate, quantity and discount.
@@ -39,11 +45,6 @@ export function updateItemsEntriesTotal(rows) {
}));
}
export const ITEM_TYPE = {
SELLABLE: 'SELLABLE',
PURCHASABLE: 'PURCHASABLE',
};
/**
* Retrieve total of the given items entries.
*/
@@ -150,12 +151,7 @@ export function useFetchItemRow({ landedCost, itemType, notifyNewRow }) {
*/
export const composeRowsOnEditCell = R.curry(
(rowIndex, columnId, value, defaultEntry, rows) => {
return compose(
orderingLinesIndexes,
updateAutoAddNewLine(defaultEntry, ['item_id']),
updateItemsEntriesTotal,
updateTableCell(rowIndex, columnId, value),
)(rows);
return compose()(rows);
},
);
@@ -171,10 +167,102 @@ export const composeRowsOnNewRow = R.curry((rowIndex, newRow, rows) => {
});
/**
*
* @param {*} entries
* Associate tax rate to entries.
*/
export const assignEntriesTaxRate = R.curry((taxRates, entries) => {
const taxRatesById = keyBy(taxRates, 'id');
return entries.map((entry) => {
const taxRate = taxRatesById[entry.tax_rate_id];
return {
...entry,
tax_rate: taxRate?.rate || 0,
};
});
});
/**
* Assign tax amount to entries.
* @param {boolean} isInclusiveTax
* @param entries
* @returns
*/
export const composeControlledEntries = (entries) => {
return R.compose(orderingLinesIndexes, updateItemsEntriesTotal)(entries);
export const assignEntriesTaxAmount = R.curry(
(isInclusiveTax: boolean, entries) => {
return entries.map((entry) => {
const taxAmount = isInclusiveTax
? getInclusiveTaxAmount(entry.amount, entry.tax_rate)
: getExlusiveTaxAmount(entry.amount, entry.tax_rate);
return {
...entry,
tax_amount: taxAmount,
};
});
},
);
/**
* 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;
};
/**
* Compose rows when edit a table cell.
* @returns {Function}
*/
export const useComposeRowsOnEditTableCell = () => {
const { taxRates, isInclusiveTax, localValue, defaultEntry } =
useItemEntriesTableContext();
return useCallback(
(rowIndex, columnId, value) => {
return R.compose(
assignEntriesTaxAmount(isInclusiveTax),
assignEntriesTaxRate(taxRates),
orderingLinesIndexes,
updateAutoAddNewLine(defaultEntry, ['item_id']),
updateItemsEntriesTotal,
updateTableCell(rowIndex, columnId, value),
)(localValue);
},
[taxRates, isInclusiveTax, localValue, defaultEntry],
);
};
/**
* Compose rows when remove a table row.
* @returns {Function}
*/
export const useComposeRowsOnRemoveTableRow = () => {
const { minLinesNumber, defaultEntry, localValue } =
useItemEntriesTableContext();
return useCallback(
(rowIndex) => {
return compose(
// Ensure minimum lines count.
updateMinEntriesLines(minLinesNumber, defaultEntry),
// Remove the line by the given index.
updateRemoveLineByIndex(rowIndex),
)(localValue);
},
[minLinesNumber, defaultEntry, localValue],
);
};

View File

@@ -9,7 +9,7 @@ const ARAgingSummaryHeaderDimensonsContext = React.createContext();
/**
* ARAging summary header dismensions provider.
* @returns
* @returns {JSX.Element}
*/
function ARAgingSummaryHeaderDimensionsProvider({ query, ...props }) {
// Features guard.

View File

@@ -44,6 +44,7 @@ export default function CashFlowStatementTable({
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
sticky={true}
expandColumnSpace={0.8}
styleName={TableStyle.Constrant}
/>

View File

@@ -35,6 +35,7 @@ export default function CustomersBalanceSummaryTable({
data={table.data}
rowClassNames={tableRowTypesToClassnames}
noInitialFetch={true}
sticky={true}
styleName={TableStyle.Constrant}
/>
</FinancialSheet>

View File

@@ -47,6 +47,7 @@ export default function CustomersTransactionsTable({
noInitialFetch={true}
expandable={true}
expanded={expandedRows}
sticky={true}
expandToggleColumn={1}
expandColumnSpace={0.8}
styleName={TableStyle.Constrant}

View File

@@ -3,11 +3,7 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { For, DashboardInsider } from '@/components';
import useFilterFinancialReports from './FilterFinancialReports';
import {
financialReportMenus,
SalesAndPurchasesReportMenus,
} from '@/constants/financialReportsMenu';
import { financialReportMenus } from '@/constants/financialReportsMenu';
import '@/style/pages/FinancialStatements/FinancialSheets.scss';
@@ -39,18 +35,11 @@ function FinancialReportsSection({ sectionTitle, reports }) {
*/
export default function FinancialReports() {
const financialReportMenu = useFilterFinancialReports(financialReportMenus);
const SalesAndPurchasesReportMenu = useFilterFinancialReports(
SalesAndPurchasesReportMenus,
);
return (
<DashboardInsider name={'financial-reports'}>
<div class="financial-reports">
<For render={FinancialReportsSection} of={financialReportMenu} />
<For
render={FinancialReportsSection}
of={SalesAndPurchasesReportMenu}
/>
</div>
</DashboardInsider>
);

View File

@@ -48,6 +48,7 @@ export function InventoryItemDetailsTable({
expanded={expandedRows}
expandToggleColumn={1}
expandColumnSpace={0.8}
sticky={true}
styleName={TableStyle.Constrant}
/>
</FinancialSheet>

View File

@@ -0,0 +1,72 @@
// @ts-nocheck
import React, { useEffect } from 'react';
import moment from 'moment';
import { SalesTaxLiabilitySummaryLoadingBar } from './components';
import { FinancialStatement, DashboardPageContent } from '@/components';
import SalesTaxLiabilitySummaryHeader from './SalesTaxLiabilitySummaryHeader';
import SalesTaxLiabilitySummaryActionsBar from './SalesTaxLiabilitySummaryActionsBar';
import { SalesTaxLiabilitySummaryBoot } from './SalesTaxLiabilitySummaryBoot';
import { SalesTaxLiabilitySummaryBody } from './SalesTaxLiabilitySummaryBody';
import { useSalesTaxLiabilitySummaryQuery } from './utils';
import withSalesTaxLiabilitySummaryActions from './withSalesTaxLiabilitySummaryActions';
import { compose } from '@/utils';
/**
* Sales tax liability summary.
* @returns {React.JSX}
*/
function SalesTaxLiabilitySummary({
// #withSalesTaxLiabilitySummaryActions
toggleSalesTaxLiabilitySummaryFilterDrawer,
}) {
const [query, setQuery] = useSalesTaxLiabilitySummaryQuery();
const handleFilterSubmit = (filter) => {
const newFilter = {
...filter,
fromDate: moment(filter.fromDate).format('YYYY-MM-DD'),
toDate: moment(filter.toDate).format('YYYY-MM-DD'),
};
setQuery({ ...newFilter });
};
// Handle number format submit.
const handleNumberFormatSubmit = (values) => {
setQuery({
...query,
numberFormat: values,
});
};
// Hides the filter drawer once the page unmount.
useEffect(
() => () => {
toggleSalesTaxLiabilitySummaryFilterDrawer(false);
},
[toggleSalesTaxLiabilitySummaryFilterDrawer],
);
return (
<SalesTaxLiabilitySummaryBoot filter={query}>
<SalesTaxLiabilitySummaryActionsBar
numberFormat={query.numberFormat}
onNumberFormatSubmit={handleNumberFormatSubmit}
/>
<SalesTaxLiabilitySummaryLoadingBar />
<DashboardPageContent>
<FinancialStatement>
<SalesTaxLiabilitySummaryHeader
pageFilter={query}
onSubmitFilter={handleFilterSubmit}
/>
<SalesTaxLiabilitySummaryBody />
</FinancialStatement>
</DashboardPageContent>
</SalesTaxLiabilitySummaryBoot>
);
}
export default compose(withSalesTaxLiabilitySummaryActions)(
SalesTaxLiabilitySummary,
);

View File

@@ -0,0 +1,131 @@
// @ts-nocheck
import React from 'react';
import {
NavbarGroup,
Button,
Classes,
NavbarDivider,
Popover,
PopoverInteractionKind,
Position,
} from '@blueprintjs/core';
import classNames from 'classnames';
import { DashboardActionsBar, FormattedMessage as T, Icon } from '@/components';
import NumberFormatDropdown from '@/components/NumberFormatDropdown';
import { compose, saveInvoke } from '@/utils';
import { useSalesTaxLiabilitySummaryContext } from './SalesTaxLiabilitySummaryBoot';
import withSalesTaxLiabilitySummary from './withSalesTaxLiabilitySummary';
import withSalesTaxLiabilitySummaryActions from './withSalesTaxLiabilitySummaryActions';
/**
* Sales tax liability summary - actions bar.
*/
function SalesTaxLiabilitySummaryActionsBar({
// #withSalesTaxLiabilitySummary
salesTaxLiabilitySummaryFilter,
// #withSalesTaxLiabilitySummaryActions
toggleSalesTaxLiabilitySummaryFilterDrawer: toggleFilterDrawer,
// #ownProps
numberFormat,
onNumberFormatSubmit,
}) {
const { isLoading, refetchSalesTaxLiabilitySummary } =
useSalesTaxLiabilitySummaryContext();
// Handle filter toggle click.
const handleFilterToggleClick = () => {
toggleFilterDrawer();
};
// Handle re-calculate the report button.
const handleRecalcReport = () => {
refetchSalesTaxLiabilitySummary();
};
// Handle number format form submit.
const handleNumberFormatSubmit = (values) => {
saveInvoke(onNumberFormatSubmit, values);
};
return (
<DashboardActionsBar>
<NavbarGroup>
<Button
className={classNames(Classes.MINIMAL, 'button--gray-highlight')}
text={<T id={'recalc_report'} />}
onClick={handleRecalcReport}
icon={<Icon icon="refresh-16" iconSize={16} />}
/>
<NavbarDivider />
<Button
className={classNames(Classes.MINIMAL, 'button--table-views')}
icon={<Icon icon="cog-16" iconSize={16} />}
text={
!salesTaxLiabilitySummaryFilter ? (
<T id={'customize_report'} />
) : (
<T id={'hide_customizer'} />
)
}
onClick={handleFilterToggleClick}
active={salesTaxLiabilitySummaryFilter}
/>
<NavbarDivider />
<Popover
content={
<NumberFormatDropdown
numberFormat={numberFormat}
onSubmit={handleNumberFormatSubmit}
submitDisabled={isLoading}
/>
}
minimal={true}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'format'} />}
icon={<Icon icon="numbers" width={23} height={16} />}
/>
</Popover>
<Popover
// content={}
interactionKind={PopoverInteractionKind.CLICK}
position={Position.BOTTOM_LEFT}
>
<Button
className={classNames(Classes.MINIMAL, 'button--filter')}
text={<T id={'filter'} />}
icon={<Icon icon="filter-16" iconSize={16} />}
/>
</Popover>
<NavbarDivider />
<Button
className={Classes.MINIMAL}
icon={<Icon icon="print-16" iconSize={16} />}
text={<T id={'print'} />}
/>
<Button
className={Classes.MINIMAL}
icon={<Icon icon="file-export-16" iconSize={16} />}
text={<T id={'export'} />}
/>
</NavbarGroup>
</DashboardActionsBar>
);
}
export default compose(
withSalesTaxLiabilitySummary(({ salesTaxLiabilitySummaryFilter }) => ({
salesTaxLiabilitySummaryFilter,
})),
withSalesTaxLiabilitySummaryActions,
)(SalesTaxLiabilitySummaryActionsBar);

View File

@@ -0,0 +1,26 @@
// @ts-nocheck
import React from 'react';
import { FinancialReportBody } from '../FinancialReportPage';
import { FinancialSheetSkeleton } from '@/components';
import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable';
import { useSalesTaxLiabilitySummaryContext } from './SalesTaxLiabilitySummaryBoot';
/**
* Sales tax liability summary body.
* @returns {React.JSX}
*/
export function SalesTaxLiabilitySummaryBody() {
const { isLoading } = useSalesTaxLiabilitySummaryContext();
return (
<FinancialReportBody>
{isLoading ? (
<FinancialSheetSkeleton />
) : (
<SalesTaxLiabilitySummaryTable />
)}
</FinancialReportBody>
);
}

View File

@@ -0,0 +1,46 @@
// @ts-nocheck
import React, { createContext, useContext } from 'react';
import FinancialReportPage from '../FinancialReportPage';
import { transformFilterFormToQuery } from '../common';
import { useSalesTaxLiabilitySummary } from '@/hooks/query';
const SalesTaxLiabilitySummaryContext = createContext();
/**
* Sales tax liability summary boot.
* @returns {JSX.Element}
*/
function SalesTaxLiabilitySummaryBoot({ filter, ...props }) {
// Transformes the given filter to query.
const query = React.useMemo(
() => transformFilterFormToQuery(filter),
[filter],
);
// Fetches the sales tax liability summary report.
const {
data: salesTaxLiabilitySummary,
isFetching,
isLoading,
refetch,
} = useSalesTaxLiabilitySummary(query, { keepPreviousData: true });
const provider = {
salesTaxLiabilitySummary,
refetchSalesTaxLiabilitySummary: refetch,
isFetching,
isLoading,
query,
filter,
};
return (
<FinancialReportPage name={'sales-tax-liability-summary'}>
<SalesTaxLiabilitySummaryContext.Provider value={provider} {...props} />
</FinancialReportPage>
);
}
const useSalesTaxLiabilitySummaryContext = () =>
useContext(SalesTaxLiabilitySummaryContext);
export { SalesTaxLiabilitySummaryBoot, useSalesTaxLiabilitySummaryContext };

View File

@@ -0,0 +1,114 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import moment from 'moment';
import { Button, Intent, Tab, Tabs } from '@blueprintjs/core';
import { Formik, Form } from 'formik';
import { FormattedMessage as T } from '@/components';
import { useFeatureCan } from '@/hooks/state';
import FinancialStatementHeader from '../../FinancialStatements/FinancialStatementHeader';
import { compose, transformToForm } from '@/utils';
import {
getDefaultSalesTaxLiablitySummaryQuery,
getSalesTaxLiabilitySummaryQueryValidation,
} from './utils';
import withSalesTaxLiabilitySummary from './withSalesTaxLiabilitySummary';
import withSalesTaxLiabilitySummaryActions from './withSalesTaxLiabilitySummaryActions';
import { SalesTaxLiabilitySummaryHeaderGeneral } from './SalesTaxLiabilitySummaryHeaderGeneralPanel';
/**
* Sales tax liability summary header.
*/
function SalesTaxLiabilitySummaryHeader({
// #ownProps
onSubmitFilter,
pageFilter,
// #withSalesTaxLiabilitySummary
salesTaxLiabilitySummaryFilter,
// #withSalesTaxLiabilitySummaryActions
toggleSalesTaxLiabilitySummaryFilterDrawer: toggleFilterDrawer,
}) {
const defaultValues = getDefaultSalesTaxLiablitySummaryQuery();
// Validation schema.
const validationSchema = getSalesTaxLiabilitySummaryQueryValidation();
// Filter form initial values.
const initialValues = transformToForm(
{
...defaultValues,
...pageFilter,
fromDate: moment(pageFilter.fromDate).toDate(),
toDate: moment(pageFilter.toDate).toDate(),
},
defaultValues,
);
// Handle form submit.
const handleSubmit = (values, actions) => {
onSubmitFilter(values);
toggleFilterDrawer(false);
actions.setSubmitting(false);
};
// Handle cancel button click.
const handleCancelClick = () => {
toggleFilterDrawer(false);
};
// Handle drawer close action.
const handleDrawerClose = () => {
toggleFilterDrawer(false);
};
// Detarmines the given feature whether is enabled.
const { featureCan } = useFeatureCan();
return (
<SalesTaxSummaryFinancialHeader
isOpen={salesTaxLiabilitySummaryFilter}
drawerProps={{
onClose: handleDrawerClose,
}}
>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
<Form>
<Tabs animate={true} vertical={true} renderActiveTabPanelOnly={true}>
<Tab
id="general"
title={<T id={'general'} />}
panel={<SalesTaxLiabilitySummaryHeaderGeneral />}
/>
</Tabs>
<div class="financial-header-drawer__footer">
<Button className={'mr1'} intent={Intent.PRIMARY} type={'submit'}>
<T id={'calculate_report'} />
</Button>
<Button onClick={handleCancelClick} minimal={true}>
<T id={'cancel'} />
</Button>
</div>
</Form>
</Formik>
</SalesTaxSummaryFinancialHeader>
);
}
export default compose(
withSalesTaxLiabilitySummary(({ salesTaxLiabilitySummaryFilter }) => ({
salesTaxLiabilitySummaryFilter,
})),
withSalesTaxLiabilitySummaryActions,
)(SalesTaxLiabilitySummaryHeader);
const SalesTaxSummaryFinancialHeader = styled(FinancialStatementHeader)`
.bp3-drawer {
max-height: 320px;
}
`;

View File

@@ -0,0 +1,13 @@
// @ts-nocheck
import React from 'react';
import FinancialStatementDateRange from '../FinancialStatementDateRange';
import RadiosAccountingBasis from '../RadiosAccountingBasis';
export function SalesTaxLiabilitySummaryHeaderGeneral() {
return (
<div>
<FinancialStatementDateRange />
<RadiosAccountingBasis key={'basis'} />
</div>
);
}

View File

@@ -0,0 +1,103 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { compose } from 'ramda';
import { TableStyle } from '@/constants';
import { ReportDataTable, FinancialSheet } from '@/components';
import { defaultExpanderReducer, tableRowTypesToClassnames } from '@/utils';
import { useSalesTaxLiabilitySummaryContext } from './SalesTaxLiabilitySummaryBoot';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import { useSalesTaxLiabilitySummaryColumns } from './utils';
/**
* Balance sheet table.
*/
function SalesTaxLiabilitySummaryTableRoot({
// #ownProps
organizationName,
}) {
// Balance sheet context.
const {
salesTaxLiabilitySummary: { table, query },
} = useSalesTaxLiabilitySummaryContext();
// Retrieve the database columns.
const columns = useSalesTaxLiabilitySummaryColumns();
// Retrieve default expanded rows of balance sheet.
const expandedRows = React.useMemo(
() => defaultExpanderReducer(table.rows, 3),
[table],
);
return (
<FinancialSheet
companyName={organizationName}
sheetType={'Sales Tax Liability Summary'}
fromDate={query.from_date}
toDate={query.to_date}
basis={''}
>
<SalesTaxLiabilitySummaryDataTable
columns={columns}
data={table.rows}
rowClassNames={tableRowTypesToClassnames}
noInitialFetch={true}
expandable={true}
expanded={expandedRows}
expandToggleColumn={1}
expandColumnSpace={0.8}
headerLoading={true}
sticky={true}
styleName={TableStyle.Constrant}
/>
</FinancialSheet>
);
}
const SalesTaxLiabilitySummaryDataTable = styled(ReportDataTable)`
.table {
.tbody .tr {
.td {
border-bottom: 0;
padding-top: 0.32rem;
padding-bottom: 0.32rem;
}
&:not(.no-results) {
.td {
border-bottom: 0;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
&:not(:first-child) .td {
border-top: 1px solid transparent;
}
&.row_type--Total {
font-weight: 500;
.td {
border-top: 1px solid #bbb;
border-bottom: 3px double #333;
}
}
&.row_type--TaxRate {
.td {
&.td-taxPercentage,
&.td-taxableAmount,
&.td-collectedTax,
&.td-taxRate {
color: #444;
}
}
}
}
}
}
`;
export const SalesTaxLiabilitySummaryTable = compose(
withCurrentOrganization(({ organization }) => ({
organizationName: organization.name,
})),
)(SalesTaxLiabilitySummaryTableRoot);

View File

@@ -0,0 +1,17 @@
// @ts-nocheck
import React from 'react';
import { useSalesTaxLiabilitySummaryContext } from './SalesTaxLiabilitySummaryBoot';
import FinancialLoadingBar from '../FinancialLoadingBar';
/**
* Sales tax liability summary loading bar.
*/
export function SalesTaxLiabilitySummaryLoadingBar() {
const { isFetching } = useSalesTaxLiabilitySummaryContext();
if (!isFetching) {
return null;
}
return <FinancialLoadingBar />;
}

View File

@@ -0,0 +1,48 @@
// @ts-nocheck
import React from 'react';
import * as R from 'ramda';
import { getColumnWidth } from '@/utils';
import { Align } from '@/constants';
const getTableCellValueAccessor = (index) => `cells[${index}].value`;
const taxNameAccessor = R.curry((data, column) => ({
key: column.key,
Header: column.label,
accessor: getTableCellValueAccessor(column.cell_index),
sticky: 'left',
width: 300,
textOverview: true,
disableSortBy: true,
}));
const taxableAmountAccessor = R.curry((data, column) => {
const accessor = getTableCellValueAccessor(column.cell_index);
return {
Header: column.label,
id: column.key,
accessor: getTableCellValueAccessor(column.cell_index),
className: column.key,
width: getColumnWidth(data, accessor, { minWidth: 120 }),
align: Align.Right,
disableSortBy: true,
};
});
const dynamicColumnMapper = R.curry((data, column) => {
const taxNameAccessorColumn = taxNameAccessor(data);
const taxableAmountColumn = taxableAmountAccessor(data);
return R.compose(
R.when(R.pathEq(['key'], 'taxName'), taxNameAccessorColumn),
R.when(R.pathEq(['key'], 'taxableAmount'), taxableAmountColumn),
R.when(R.pathEq(['key'], 'taxRate'), taxableAmountColumn),
R.when(R.pathEq(['key'], 'taxPercentage'), taxableAmountColumn),
R.when(R.pathEq(['key'], 'collectedTax'), taxableAmountColumn),
)(column);
});
export const salesTaxLiabilitySummaryDynamicColumns = (columns, data) => {
return R.map(dynamicColumnMapper(data), columns);
};

View File

@@ -0,0 +1,89 @@
// @ts-nocheck
import React from 'react';
import moment from 'moment';
import * as Yup from 'yup';
import { castArray } from 'lodash';
import intl from 'react-intl-universal';
import { transformToForm } from '@/utils';
import { useAppQueryString } from '@/hooks';
import { salesTaxLiabilitySummaryDynamicColumns } from './dynamicColumns';
import { useSalesTaxLiabilitySummaryContext } from './SalesTaxLiabilitySummaryBoot';
/**
* Retrieves the default sales tax liability summary query.
* @returns {}
*/
export const getDefaultSalesTaxLiablitySummaryQuery = () => ({
fromDate: moment().startOf('month').format('YYYY-MM-DD'),
toDate: moment().format('YYYY-MM-DD'),
basis: 'cash',
});
/**
* Parses the sales tax liability summary query.
*/
const parseSalesTaxLiabilitySummaryQuery = (locationQuery) => {
const defaultQuery = getDefaultSalesTaxLiablitySummaryQuery();
const transformed = {
...defaultQuery,
...transformToForm(locationQuery, defaultQuery),
};
return {
...transformed,
// Ensures the branches ids is always array.
branchesIds: castArray(transformed.branchesIds),
};
};
/**
* Retrieves the sales tax liability summary query.
*/
export const useSalesTaxLiabilitySummaryQuery = () => {
// Retrieves location query.
const [locationQuery, setLocationQuery] = useAppQueryString();
// Merges the default filter query with location URL query.
const parsedQuery = React.useMemo(
() => parseSalesTaxLiabilitySummaryQuery(locationQuery),
[locationQuery],
);
return [parsedQuery, setLocationQuery];
};
/**
* Retrieves the sales tax liability summary default query.
*/
export const getSalesTaxLiabilitySummaryDefaultQuery = () => {
return {
basic: 'cash',
fromDate: moment().toDate(),
toDate: moment().toDate(),
};
};
/**
* Retrieves the sales tax liability summary query validation.
*/
export const getSalesTaxLiabilitySummaryQueryValidation = () =>
Yup.object().shape({
dateRange: Yup.string().optional(),
fromDate: Yup.date().required().label(intl.get('fromDate')),
toDate: Yup.date()
.min(Yup.ref('fromDate'))
.required()
.label(intl.get('toDate')),
});
/**
* Retrieves the sales tax liability summary columns.
* @returns {ITableColumn[]}
*/
export const useSalesTaxLiabilitySummaryColumns = () => {
const {
salesTaxLiabilitySummary: { table },
} = useSalesTaxLiabilitySummaryContext();
return salesTaxLiabilitySummaryDynamicColumns(table.columns, table.rows);
};

Some files were not shown because too many files have changed in this diff Show More