Compare commits
56 Commits
fix-filter
...
v0.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
597973d5d1 | ||
|
|
ee6bc822c9 | ||
|
|
07e78ebd6a | ||
|
|
8c04a6205b | ||
|
|
d54ea68d46 | ||
|
|
f42c8031c2 | ||
|
|
5261d332b7 | ||
|
|
f1e7d0fc44 | ||
|
|
1d1049043e | ||
|
|
1148fef9ad | ||
|
|
92ac0dbd25 | ||
|
|
ce41845bd7 | ||
|
|
eaf72d1608 | ||
|
|
ac336f9878 | ||
|
|
746c80c564 | ||
|
|
601434b107 | ||
|
|
5aaa33e585 | ||
|
|
d48d864a5f | ||
|
|
453df2ac4e | ||
|
|
73ceeaee46 | ||
|
|
1b4b656419 | ||
|
|
df823c0bfe | ||
|
|
e91d7b0cff | ||
|
|
aa52e7d02c | ||
|
|
4e53d08497 | ||
|
|
2356921f27 | ||
|
|
a0a5d00be3 | ||
|
|
fbd74c559b | ||
|
|
8a64198433 | ||
|
|
b98b73ad98 | ||
|
|
6abae43c6f | ||
|
|
7657337c4f | ||
|
|
983ceb5cc6 | ||
|
|
ac072d29fc | ||
|
|
17e055db5e | ||
|
|
b49a021506 | ||
|
|
b49b45fb43 | ||
|
|
bb7cf41e3e | ||
|
|
801ea5dfdb | ||
|
|
eb03a38553 | ||
|
|
0852feecbf | ||
|
|
54dcde657f | ||
|
|
5bb95eeb1a | ||
|
|
6baec8dd96 | ||
|
|
6535424d0f | ||
|
|
09d73db20f | ||
|
|
7f746b96c8 | ||
|
|
ed2bca6b74 | ||
|
|
f9d5a3c69a | ||
|
|
d1121f0b81 | ||
|
|
a7644e6481 | ||
|
|
d6f56568a3 | ||
|
|
04d134806b | ||
|
|
339559d830 | ||
|
|
22e4d757e4 | ||
|
|
3b98e8de80 |
13
.github/FUNDING.yml
vendored
Normal file
13
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: Bigcapital # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,4 +1,9 @@
|
||||
node_modules/
|
||||
data
|
||||
|
||||
# Docker volumes data directory
|
||||
/data
|
||||
|
||||
# Production env file
|
||||
.env
|
||||
|
||||
test-results/
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -20,6 +20,7 @@ import InventoryDetailsController from './FinancialStatements/InventoryDetails';
|
||||
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
|
||||
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
|
||||
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
|
||||
import SalesTaxLiabilitySummary from './FinancialStatements/SalesTaxLiabilitySummary';
|
||||
|
||||
@Service()
|
||||
export default class FinancialStatementsService {
|
||||
@@ -68,40 +69,44 @@ export default class FinancialStatementsService {
|
||||
);
|
||||
router.use(
|
||||
'/customer-balance-summary',
|
||||
Container.get(CustomerBalanceSummaryController).router(),
|
||||
Container.get(CustomerBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/vendor-balance-summary',
|
||||
Container.get(VendorBalanceSummaryController).router(),
|
||||
Container.get(VendorBalanceSummaryController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-customers',
|
||||
Container.get(TransactionsByCustomers).router(),
|
||||
Container.get(TransactionsByCustomers).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-vendors',
|
||||
Container.get(TransactionsByVendors).router(),
|
||||
Container.get(TransactionsByVendors).router()
|
||||
);
|
||||
router.use(
|
||||
'/cash-flow',
|
||||
Container.get(CashFlowStatementController).router(),
|
||||
Container.get(CashFlowStatementController).router()
|
||||
);
|
||||
router.use(
|
||||
'/inventory-item-details',
|
||||
Container.get(InventoryDetailsController).router(),
|
||||
Container.get(InventoryDetailsController).router()
|
||||
);
|
||||
router.use(
|
||||
'/transactions-by-reference',
|
||||
Container.get(TransactionsByReferenceController).router(),
|
||||
Container.get(TransactionsByReferenceController).router()
|
||||
);
|
||||
router.use(
|
||||
'/cashflow-account-transactions',
|
||||
Container.get(CashflowAccountTransactions).router(),
|
||||
Container.get(CashflowAccountTransactions).router()
|
||||
);
|
||||
router.use(
|
||||
'/project-profitability-summary',
|
||||
Container.get(ProjectProfitabilityController).router(),
|
||||
)
|
||||
Container.get(ProjectProfitabilityController).router()
|
||||
);
|
||||
router.use(
|
||||
'/sales-tax-liability-summary',
|
||||
Container.get(SalesTaxLiabilitySummary).router()
|
||||
);
|
||||
return router;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { query } from 'express-validator';
|
||||
import { Inject } from 'typedi';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import BaseFinancialReportController from '../BaseFinancialReportController';
|
||||
import { AbilitySubject, ReportsAction } from '@/interfaces';
|
||||
import CheckPolicies from '@/api/middleware/CheckPolicies';
|
||||
import { SalesTaxLiabilitySummaryService } from '@/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryService';
|
||||
|
||||
export default class SalesTaxLiabilitySummary extends BaseFinancialReportController {
|
||||
@Inject()
|
||||
private salesTaxLiabilitySummaryService: SalesTaxLiabilitySummaryService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
CheckPolicies(
|
||||
ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
AbilitySubject.Report
|
||||
),
|
||||
this.validationSchema,
|
||||
asyncMiddleware(this.salesTaxLiabilitySummary.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation schema.
|
||||
*/
|
||||
get validationSchema() {
|
||||
return [
|
||||
query('from_date').optional().isISO8601(),
|
||||
query('to_date').optional().isISO8601(),
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* Retrieves the sales tax liability summary.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
async salesTaxLiabilitySummary(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const filter = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const accept = this.accepts(req);
|
||||
const acceptType = accept.types(['json', 'application/json+table']);
|
||||
|
||||
switch (acceptType) {
|
||||
case 'application/json+table':
|
||||
const salesTaxLiabilityTable =
|
||||
await this.salesTaxLiabilitySummaryService.salesTaxLiabilitySummaryTable(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
table: salesTaxLiabilityTable.table,
|
||||
query: salesTaxLiabilityTable.query,
|
||||
meta: salesTaxLiabilityTable.meta,
|
||||
});
|
||||
case 'json':
|
||||
default:
|
||||
const salesTaxLiability =
|
||||
await this.salesTaxLiabilitySummaryService.salesTaxLiability(
|
||||
tenantId,
|
||||
filter
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: salesTaxLiability.data,
|
||||
query: salesTaxLiability.query,
|
||||
meta: salesTaxLiability.meta,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,8 +169,9 @@ export default class SaleInvoicesController extends BaseController {
|
||||
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
check('project_id').optional({ nullable: true }).isNumeric().toInt(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('is_inclusive_tax').optional().isBoolean().toBoolean(),
|
||||
|
||||
check('entries').exists().isArray({ min: 1 }),
|
||||
check('entries.*.index').exists().isNumeric().toInt(),
|
||||
check('entries.*.item_id').exists().isNumeric().toInt(),
|
||||
check('entries.*.rate').exists().isNumeric().toFloat(),
|
||||
@@ -183,6 +184,15 @@ export default class SaleInvoicesController extends BaseController {
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape(),
|
||||
check('entries.*.tax_code')
|
||||
.optional({ nullable: true })
|
||||
.trim()
|
||||
.escape()
|
||||
.isString(),
|
||||
check('entries.*.tax_rate_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
.toInt(),
|
||||
check('entries.*.warehouse_id')
|
||||
.optional({ nullable: true })
|
||||
.isNumeric()
|
||||
@@ -756,6 +766,16 @@ export default class SaleInvoicesController extends BaseController {
|
||||
],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND', code: 5000 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND') {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND', code: 5100 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
278
packages/server/src/api/controllers/TaxRates/TaxRates.ts
Normal file
278
packages/server/src/api/controllers/TaxRates/TaxRates.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { body, param } from 'express-validator';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
|
||||
import { TaxRatesApplication } from '@/services/TaxRates/TaxRatesApplication';
|
||||
import CheckAbilities from '@/api/middleware/CheckPolicies';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from '@/services/TaxRates/constants';
|
||||
import { AbilitySubject, TaxRateAction } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class TaxRatesController extends BaseController {
|
||||
@Inject()
|
||||
private taxRatesApplication: TaxRatesApplication;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
CheckAbilities(TaxRateAction.CREATE, AbilitySubject.TaxRate),
|
||||
this.taxRateValidationSchema,
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.createTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt(), ...this.taxRateValidationSchema],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.editTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/active',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.activateTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:id/inactive',
|
||||
CheckAbilities(TaxRateAction.EDIT, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.inactivateTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.delete(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.DELETE, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.deleteTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:id',
|
||||
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
|
||||
[param('id').exists().toInt()],
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTaxRate.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
CheckAbilities(TaxRateAction.VIEW, AbilitySubject.TaxRate),
|
||||
this.validationResult,
|
||||
asyncMiddleware(this.getTaxRates.bind(this)),
|
||||
this.handleServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax rate validation schema.
|
||||
*/
|
||||
private get taxRateValidationSchema() {
|
||||
return [
|
||||
body('name').exists(),
|
||||
body('code').exists().isString(),
|
||||
body('rate').exists().isNumeric().toFloat(),
|
||||
body('description').optional().trim().isString(),
|
||||
body('is_non_recoverable').optional().isBoolean().default(false),
|
||||
body('is_compound').optional().isBoolean().default(false),
|
||||
body('active').optional().isBoolean().default(false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async createTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const createTaxRateDTO = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.createTaxRate(
|
||||
tenantId,
|
||||
createTaxRateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: taxRate,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async editTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const editTaxRateDTO = this.matchedBodyData(req);
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.editTaxRate(
|
||||
tenantId,
|
||||
taxRateId,
|
||||
editTaxRateDTO
|
||||
);
|
||||
return res.status(200).send({
|
||||
data: taxRate,
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async deleteTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.deleteTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
code: 200,
|
||||
message: 'The tax rate has been deleted successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async getTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
const taxRate = await this.taxRatesApplication.getTaxRate(
|
||||
tenantId,
|
||||
taxRateId
|
||||
);
|
||||
return res.status(200).send({ data: taxRate });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
*/
|
||||
public async getTaxRates(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const taxRates = await this.taxRatesApplication.getTaxRates(tenantId);
|
||||
|
||||
return res.status(200).send({ data: taxRates });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param next
|
||||
* @returns
|
||||
*/
|
||||
public async inactivateTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.inactivateTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: taxRateId,
|
||||
message: 'The given tax rate has been inactivated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
* @returns
|
||||
*/
|
||||
public async activateTaxRate(req: Request, res: Response, next) {
|
||||
const { tenantId } = req;
|
||||
const { id: taxRateId } = req.params;
|
||||
|
||||
try {
|
||||
await this.taxRatesApplication.activateTaxRate(tenantId, taxRateId);
|
||||
|
||||
return res.status(200).send({
|
||||
id: taxRateId,
|
||||
message: 'The given tax rate has been activated successfully.',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles service errors.
|
||||
* @param {Error} error
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private handleServiceErrors(error: Error, req: Request, res: Response, next) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === ERRORS.TAX_CODE_NOT_UNIQUE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_CODE_NOT_UNIQUE, code: 100 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_NOT_FOUND) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_NOT_FOUND, code: 200 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_ALREADY_INACTIVE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_ALREADY_INACTIVE, code: 300 }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === ERRORS.TAX_RATE_ALREADY_ACTIVE) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [{ type: ERRORS.TAX_RATE_ALREADY_ACTIVE, code: 400 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,7 @@ import { InventoryItemsCostController } from './controllers/Inventory/Inventorty
|
||||
import { ProjectsController } from './controllers/Projects/Projects';
|
||||
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
||||
import { ProjectTimesController } from './controllers/Projects/Times';
|
||||
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
|
||||
|
||||
export default () => {
|
||||
const app = Router();
|
||||
@@ -129,6 +130,7 @@ export default () => {
|
||||
);
|
||||
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||
|
||||
|
||||
28
packages/server/src/data/TransactionTypes.ts
Executable file
28
packages/server/src/data/TransactionTypes.ts
Executable file
@@ -0,0 +1,28 @@
|
||||
export const TransactionTypes = {
|
||||
SaleInvoice: 'Sale invoice',
|
||||
SaleReceipt: 'Sale receipt',
|
||||
PaymentReceive: 'Payment receive',
|
||||
Bill: 'Bill',
|
||||
BillPayment: 'Payment made',
|
||||
VendorOpeningBalance: 'Vendor opening balance',
|
||||
CustomerOpeningBalance: 'Customer opening balance',
|
||||
InventoryAdjustment: 'Inventory adjustment',
|
||||
ManualJournal: 'Manual journal',
|
||||
Journal: 'Manual journal',
|
||||
Expense: 'Expense',
|
||||
OwnerContribution: 'Owner contribution',
|
||||
TransferToAccount: 'Transfer to account',
|
||||
TransferFromAccount: 'Transfer from account',
|
||||
OtherIncome: 'Other income',
|
||||
OtherExpense: 'Other expense',
|
||||
OwnerDrawing: 'Owner drawing',
|
||||
InvoiceWriteOff: 'Invoice write-off',
|
||||
|
||||
CreditNote: 'transaction_type.credit_note',
|
||||
VendorCredit: 'transaction_type.vendor_credit',
|
||||
|
||||
RefundCreditNote: 'transaction_type.refund_credit_note',
|
||||
RefundVendorCredit: 'transaction_type.refund_vendor_credit',
|
||||
|
||||
LandedCost: 'transaction_type.landed_cost',
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
exports.up = (knex) => {
|
||||
return knex.schema
|
||||
.createTable('tax_rates', (table) => {
|
||||
table.increments();
|
||||
table.string('name');
|
||||
table.string('code');
|
||||
table.decimal('rate');
|
||||
table.string('description');
|
||||
table.boolean('is_non_recoverable').defaultTo(false);
|
||||
table.boolean('is_compound').defaultTo(false);
|
||||
table.boolean('active').defaultTo(false);
|
||||
table.date('deleted_at');
|
||||
table.timestamps();
|
||||
})
|
||||
.table('items_entries', (table) => {
|
||||
table.boolean('is_inclusive_tax').defaultTo(false);
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.decimal('tax_rate').unsigned();
|
||||
})
|
||||
.table('sales_invoices', (table) => {
|
||||
table.boolean('is_inclusive_tax').defaultTo(false);
|
||||
table.decimal('tax_amount_withheld');
|
||||
})
|
||||
.createTable('tax_rate_transactions', (table) => {
|
||||
table.increments('id');
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.string('reference_type');
|
||||
table.integer('reference_id');
|
||||
table.decimal('rate').unsigned();
|
||||
table.integer('tax_account_id').unsigned();
|
||||
})
|
||||
.table('accounts_transactions', (table) => {
|
||||
table
|
||||
.integer('tax_rate_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('tax_rates');
|
||||
table.decimal('tax_rate').unsigned();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = (knex) => {
|
||||
return knex.schema.dropTableIfExists('tax_rates');
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { TenantSeeder } from '@/lib/Seeder/TenantSeeder';
|
||||
import { InitialTaxRates } from '../data/TaxRates';
|
||||
|
||||
export default class SeedTaxRates extends TenantSeeder {
|
||||
/**
|
||||
* Seeds initial tax rates to the organization.
|
||||
*/
|
||||
up(knex) {
|
||||
return knex('tax_rates').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('tax_rates').insert(InitialTaxRates);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TenantSeeder } from '@/lib/Seeder/TenantSeeder';
|
||||
import { InitialTaxRates } from '../data/TaxRates';
|
||||
|
||||
export default class UpdateTaxPayableAccount extends TenantSeeder {
|
||||
/**
|
||||
* Seeds initial tax rates to the organization.
|
||||
*/
|
||||
up(knex) {
|
||||
return knex('accounts').then(async () => {
|
||||
// Inserts seed entries.
|
||||
return knex('accounts').where('slug', 'tax-payable').update({
|
||||
account_type: 'tax-payable',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
30
packages/server/src/database/seeds/data/TaxRates.ts
Normal file
30
packages/server/src/database/seeds/data/TaxRates.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
export const InitialTaxRates = [
|
||||
{
|
||||
name: 'Tax Exempt',
|
||||
code: 'TAX-EXEMPT',
|
||||
description: 'Exempts goods or services from taxes.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Tax on Purchases',
|
||||
code: 'TAX-PURCHASES',
|
||||
description: 'Fee added to the cost when you buy items.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Tax on Sales',
|
||||
code: 'TAX-SALES',
|
||||
description: 'Fee added to the cost when you sell items.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
{
|
||||
name: 'Sales Tax on Imports',
|
||||
code: 'TAX-IMPORTS',
|
||||
description: 'Fee added to the cost when you sale to another country.',
|
||||
rate: 0,
|
||||
active: 1,
|
||||
},
|
||||
];
|
||||
@@ -1,7 +1,17 @@
|
||||
export const TaxPayableAccount = {
|
||||
name: 'Tax Payable',
|
||||
slug: 'tax-payable',
|
||||
account_type: 'tax-payable',
|
||||
code: '20006',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
name:'Bank Account',
|
||||
name: 'Bank Account',
|
||||
slug: 'bank-account',
|
||||
account_type: 'bank',
|
||||
code: '10001',
|
||||
@@ -11,7 +21,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Saving Bank Account',
|
||||
name: 'Saving Bank Account',
|
||||
slug: 'saving-bank-account',
|
||||
account_type: 'bank',
|
||||
code: '10002',
|
||||
@@ -21,7 +31,7 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Undeposited Funds',
|
||||
name: 'Undeposited Funds',
|
||||
slug: 'undeposited-funds',
|
||||
account_type: 'cash',
|
||||
code: '10003',
|
||||
@@ -31,7 +41,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Petty Cash',
|
||||
name: 'Petty Cash',
|
||||
slug: 'petty-cash',
|
||||
account_type: 'cash',
|
||||
code: '10004',
|
||||
@@ -41,7 +51,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Computer Equipment',
|
||||
name: 'Computer Equipment',
|
||||
slug: 'computer-equipment',
|
||||
code: '10005',
|
||||
account_type: 'fixed-asset',
|
||||
@@ -52,7 +62,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Office Equipment',
|
||||
name: 'Office Equipment',
|
||||
slug: 'office-equipment',
|
||||
code: '10006',
|
||||
account_type: 'fixed-asset',
|
||||
@@ -63,7 +73,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Accounts Receivable (A/R)',
|
||||
name: 'Accounts Receivable (A/R)',
|
||||
slug: 'accounts-receivable',
|
||||
account_type: 'accounts-receivable',
|
||||
code: '10007',
|
||||
@@ -73,7 +83,7 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Inventory Asset',
|
||||
name: 'Inventory Asset',
|
||||
slug: 'inventory-asset',
|
||||
code: '10008',
|
||||
account_type: 'inventory',
|
||||
@@ -81,12 +91,13 @@ export default [
|
||||
parent_account_id: null,
|
||||
index: 1,
|
||||
active: 1,
|
||||
description:'An account that holds valuation of products or goods that available for sale.',
|
||||
description:
|
||||
'An account that holds valuation of products or goods that available for sale.',
|
||||
},
|
||||
|
||||
// Libilities
|
||||
{
|
||||
name:'Accounts Payable (A/P)',
|
||||
name: 'Accounts Payable (A/P)',
|
||||
slug: 'accounts-payable',
|
||||
account_type: 'accounts-payable',
|
||||
parent_account_id: null,
|
||||
@@ -97,38 +108,39 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Owner A Drawings',
|
||||
name: 'Owner A Drawings',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
code: '20002',
|
||||
description:'Withdrawals by the owners.',
|
||||
description: 'Withdrawals by the owners.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Loan',
|
||||
name: 'Loan',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20003',
|
||||
description:'Money that has been borrowed from a creditor.',
|
||||
description: 'Money that has been borrowed from a creditor.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Opening Balance Liabilities',
|
||||
name: 'Opening Balance Liabilities',
|
||||
slug: 'opening-balance-liabilities',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20004',
|
||||
description:'This account will hold the difference in the debits and credits entered during the opening balance..',
|
||||
description:
|
||||
'This account will hold the difference in the debits and credits entered during the opening balance..',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Revenue Received in Advance',
|
||||
name: 'Revenue Received in Advance',
|
||||
slug: 'revenue-received-in-advance',
|
||||
account_type: 'other-current-liability',
|
||||
parent_account_id: null,
|
||||
@@ -138,34 +150,27 @@ export default [
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Sales Tax Payable',
|
||||
slug: 'owner-drawings',
|
||||
account_type: 'other-current-liability',
|
||||
code: '20006',
|
||||
description: '',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
TaxPayableAccount,
|
||||
|
||||
// Equity
|
||||
{
|
||||
name:'Retained Earnings',
|
||||
name: 'Retained Earnings',
|
||||
slug: 'retained-earnings',
|
||||
account_type: 'equity',
|
||||
code: '30001',
|
||||
description:'Retained earnings tracks net income from previous fiscal years.',
|
||||
description:
|
||||
'Retained earnings tracks net income from previous fiscal years.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Opening Balance Equity',
|
||||
name: 'Opening Balance Equity',
|
||||
slug: 'opening-balance-equity',
|
||||
account_type: 'equity',
|
||||
code: '30002',
|
||||
description:'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
|
||||
description:
|
||||
'When you enter opening balances to the accounts, the amounts enter in Opening balance equity. This ensures that you have a correct trial balance sheet for your company, without even specific the second credit or debit entry.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
@@ -181,11 +186,12 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:`Drawings`,
|
||||
name: `Drawings`,
|
||||
slug: 'drawings',
|
||||
account_type: 'equity',
|
||||
code: '30003',
|
||||
description:'Goods purchased with the intention of selling these to customers',
|
||||
description:
|
||||
'Goods purchased with the intention of selling these to customers',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
@@ -193,7 +199,7 @@ export default [
|
||||
|
||||
// Expenses
|
||||
{
|
||||
name:'Other Expenses',
|
||||
name: 'Other Expenses',
|
||||
slug: 'other-expenses',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
@@ -204,18 +210,18 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Cost of Goods Sold',
|
||||
name: 'Cost of Goods Sold',
|
||||
slug: 'cost-of-goods-sold',
|
||||
account_type: 'cost-of-goods-sold',
|
||||
parent_account_id: null,
|
||||
code: '40002',
|
||||
description:'Tracks the direct cost of the goods sold.',
|
||||
description: 'Tracks the direct cost of the goods sold.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Office expenses',
|
||||
name: 'Office expenses',
|
||||
slug: 'office-expenses',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -226,7 +232,7 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Rent',
|
||||
name: 'Rent',
|
||||
slug: 'rent',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -237,29 +243,30 @@ export default [
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Exchange Gain or Loss',
|
||||
name: 'Exchange Gain or Loss',
|
||||
slug: 'exchange-grain-loss',
|
||||
account_type: 'other-expense',
|
||||
parent_account_id: null,
|
||||
code: '40005',
|
||||
description:'Tracks the gain and losses of the exchange differences.',
|
||||
description: 'Tracks the gain and losses of the exchange differences.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Bank Fees and Charges',
|
||||
name: 'Bank Fees and Charges',
|
||||
slug: 'bank-fees-and-charges',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
code: '40006',
|
||||
description: 'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.',
|
||||
description:
|
||||
'Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
},
|
||||
{
|
||||
name:'Depreciation Expense',
|
||||
name: 'Depreciation Expense',
|
||||
slug: 'depreciation-expense',
|
||||
account_type: 'expense',
|
||||
parent_account_id: null,
|
||||
@@ -272,7 +279,7 @@ export default [
|
||||
|
||||
// Income
|
||||
{
|
||||
name:'Sales of Product Income',
|
||||
name: 'Sales of Product Income',
|
||||
slug: 'sales-of-product-income',
|
||||
account_type: 'income',
|
||||
predefined: 1,
|
||||
@@ -283,7 +290,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Sales of Service Income',
|
||||
name: 'Sales of Service Income',
|
||||
slug: 'sales-of-service-income',
|
||||
account_type: 'income',
|
||||
predefined: 0,
|
||||
@@ -294,7 +301,7 @@ export default [
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
name:'Uncategorized Income',
|
||||
name: 'Uncategorized Income',
|
||||
slug: 'uncategorized-income',
|
||||
account_type: 'income',
|
||||
parent_account_id: null,
|
||||
@@ -305,14 +312,15 @@ export default [
|
||||
predefined: 1,
|
||||
},
|
||||
{
|
||||
name:'Other Income',
|
||||
name: 'Other Income',
|
||||
slug: 'other-income',
|
||||
account_type: 'other-income',
|
||||
parent_account_id: null,
|
||||
code: '50004',
|
||||
description:'The income activities are not associated to the core business.',
|
||||
description:
|
||||
'The income activities are not associated to the core business.',
|
||||
active: 1,
|
||||
index: 1,
|
||||
predefined: 0,
|
||||
}
|
||||
];
|
||||
},
|
||||
];
|
||||
|
||||
@@ -77,6 +77,9 @@ export interface IAccountTransaction {
|
||||
projectId?: number;
|
||||
|
||||
account?: IAccount;
|
||||
|
||||
taxRateId?: number;
|
||||
taxRate?: number;
|
||||
}
|
||||
export interface IAccountResponse extends IAccount {}
|
||||
|
||||
@@ -150,3 +153,11 @@ export enum AccountAction {
|
||||
VIEW = 'View',
|
||||
TransactionsLocking = 'TransactionsLocking',
|
||||
}
|
||||
|
||||
|
||||
export enum TaxRateAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
@@ -37,6 +37,7 @@ export enum ReportsAction {
|
||||
READ_INVENTORY_ITEM_DETAILS = 'read-inventory-item-details',
|
||||
READ_CASHFLOW_ACCOUNT_TRANSACTION = 'read-cashflow-account-transactions',
|
||||
READ_PROJECT_PROFITABILITY_SUMMARY = 'read-project-profitability-summary',
|
||||
READ_SALES_TAX_LIABILITY_SUMMARY = 'read-sales-tax-liability-summary',
|
||||
}
|
||||
|
||||
export interface IFinancialSheetBranchesQuery {
|
||||
|
||||
@@ -18,6 +18,11 @@ export interface IItemEntry {
|
||||
rate: number;
|
||||
amount: number;
|
||||
|
||||
total: number;
|
||||
amountInclusingTax: number;
|
||||
amountExludingTax: number;
|
||||
discountAmount: number;
|
||||
|
||||
landedCost: number;
|
||||
allocatedCostAmount: number;
|
||||
unallocatedCostAmount: number;
|
||||
@@ -32,6 +37,10 @@ export interface IItemEntry {
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxRateId: number | null;
|
||||
taxRate: number;
|
||||
taxAmount: number;
|
||||
|
||||
item?: IItem;
|
||||
|
||||
allocatedCostEntries?: IBillLandedCostEntry[];
|
||||
@@ -46,6 +55,9 @@ export interface IItemEntryDTO {
|
||||
projectRefId?: number;
|
||||
projectRefType?: ProjectLinkRefType;
|
||||
projectRefInvoicedAmount?: number;
|
||||
|
||||
taxRateId?: number;
|
||||
taxCode?: string;
|
||||
}
|
||||
|
||||
export enum ProjectLinkRefType {
|
||||
|
||||
@@ -48,6 +48,9 @@ export interface ILedgerEntry {
|
||||
branchId?: number;
|
||||
projectId?: number;
|
||||
|
||||
taxRateId?: number;
|
||||
taxRate?: number;
|
||||
|
||||
entryId?: number;
|
||||
createdAt?: Date;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Knex } from 'knex';
|
||||
import { IItemEntry } from './ItemEntry';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
|
||||
|
||||
export interface ISaleEstimate {
|
||||
@@ -29,7 +29,7 @@ export interface ISaleEstimateDTO {
|
||||
estimateDate?: Date;
|
||||
reference?: string;
|
||||
estimateNumber?: string;
|
||||
entries: IItemEntry[];
|
||||
entries: IItemEntryDTO[];
|
||||
note: string;
|
||||
termsConditions: string;
|
||||
sendToEmail: string;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Knex } from 'knex';
|
||||
import { ISystemUser, IAccount } from '@/interfaces';
|
||||
import { ISystemUser, IAccount, ITaxTransaction } from '@/interfaces';
|
||||
import { IDynamicListFilter } from '@/interfaces/DynamicFilter';
|
||||
import { IItemEntry, IItemEntryDTO } from './ItemEntry';
|
||||
|
||||
export interface ISaleInvoice {
|
||||
id: number;
|
||||
balance: number;
|
||||
amount: number;
|
||||
amountLocal?: number;
|
||||
paymentAmount: number;
|
||||
currencyCode: string;
|
||||
exchangeRate?: number;
|
||||
@@ -27,12 +28,21 @@ export interface ISaleInvoice {
|
||||
branchId?: number;
|
||||
projectId?: number;
|
||||
|
||||
localAmount?: number;
|
||||
|
||||
localWrittenoffAmount?: number;
|
||||
writtenoffAmount?: number;
|
||||
writtenoffAmountLocal?: number;
|
||||
writtenoffExpenseAccountId?: number;
|
||||
|
||||
writtenoffExpenseAccount?: IAccount;
|
||||
|
||||
taxAmountWithheld: number;
|
||||
taxAmountWithheldLocal: number;
|
||||
taxes: ITaxTransaction[];
|
||||
|
||||
total: number;
|
||||
totalLocal: number;
|
||||
|
||||
subtotal: number;
|
||||
subtotalLocal: number;
|
||||
subtotalExludingTax: number;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceDTO {
|
||||
@@ -44,12 +54,15 @@ export interface ISaleInvoiceDTO {
|
||||
exchangeRate?: number;
|
||||
invoiceMessage: string;
|
||||
termsConditions: string;
|
||||
isTaxExclusive: boolean;
|
||||
entries: IItemEntryDTO[];
|
||||
delivered: boolean;
|
||||
|
||||
warehouseId?: number | null;
|
||||
projectId?: number;
|
||||
branchId?: number | null;
|
||||
|
||||
isInclusiveTax?: boolean;
|
||||
}
|
||||
|
||||
export interface ISaleInvoiceCreateDTO extends ISaleInvoiceDTO {
|
||||
|
||||
51
packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
Normal file
51
packages/server/src/interfaces/SalesTaxLiabilitySummary.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export interface SalesTaxLiabilitySummaryQuery {
|
||||
fromDate: Date;
|
||||
toDate: Date;
|
||||
basis: 'cash' | 'accrual';
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryAmount {
|
||||
amount: number;
|
||||
formattedAmount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryTotal {
|
||||
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryRate {
|
||||
id: number;
|
||||
taxName: string;
|
||||
taxableAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
taxPercentage: any;
|
||||
collectedTaxAmount: SalesTaxLiabilitySummaryAmount;
|
||||
}
|
||||
|
||||
export enum SalesTaxLiabilitySummaryTableRowType {
|
||||
TaxRate = 'TaxRate',
|
||||
Total = 'Total',
|
||||
}
|
||||
|
||||
export interface SalesTaxLiabilitySummaryReportData {
|
||||
taxRates: SalesTaxLiabilitySummaryRate[];
|
||||
total: SalesTaxLiabilitySummaryTotal;
|
||||
}
|
||||
|
||||
export type SalesTaxLiabilitySummaryPayableById = Record<
|
||||
string,
|
||||
{ taxRateId: number; credit: number; debit: number }
|
||||
>;
|
||||
|
||||
export type SalesTaxLiabilitySummarySalesById = Record<
|
||||
string,
|
||||
{ taxRateId: number; credit: number; debit: number }
|
||||
>;
|
||||
|
||||
export interface SalesTaxLiabilitySummaryMeta {
|
||||
organizationName: string;
|
||||
baseCurrency: string;
|
||||
}
|
||||
88
packages/server/src/interfaces/TaxRate.ts
Normal file
88
packages/server/src/interfaces/TaxRate.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Knex } from 'knex';
|
||||
|
||||
export interface ITaxRate {
|
||||
id?: number;
|
||||
name: string;
|
||||
code: string;
|
||||
rate: number;
|
||||
description: string;
|
||||
IsNonRecoverable: boolean;
|
||||
IsCompound: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ICommonTaxRateDTO {
|
||||
name: string;
|
||||
code: string;
|
||||
rate: number;
|
||||
description: string;
|
||||
IsNonRecoverable: boolean;
|
||||
IsCompound: boolean;
|
||||
active: boolean;
|
||||
}
|
||||
export interface ICreateTaxRateDTO extends ICommonTaxRateDTO {}
|
||||
export interface IEditTaxRateDTO extends ICommonTaxRateDTO {}
|
||||
|
||||
export interface ITaxRateCreatingPayload {
|
||||
createTaxRateDTO: ICreateTaxRateDTO;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateCreatedPayload {
|
||||
createTaxRateDTO: ICreateTaxRateDTO;
|
||||
taxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateEditingPayload {
|
||||
editTaxRateDTO: IEditTaxRateDTO;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateEditedPayload {
|
||||
editTaxRateDTO: IEditTaxRateDTO;
|
||||
oldTaxRate: ITaxRate;
|
||||
taxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateDeletingPayload {
|
||||
oldTaxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateActivatingPayload {
|
||||
taxRateId: number;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ITaxRateActivatedPayload {
|
||||
taxRateId: number;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxRateDeletedPayload {
|
||||
oldTaxRate: ITaxRate;
|
||||
tenantId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ITaxTransaction {
|
||||
id?: number;
|
||||
taxRateId: number;
|
||||
referenceType: string;
|
||||
referenceId: number;
|
||||
rate: number;
|
||||
taxAccountId: number;
|
||||
}
|
||||
|
||||
export enum TaxRateAction {
|
||||
CREATE = 'Create',
|
||||
EDIT = 'Edit',
|
||||
DELETE = 'Delete',
|
||||
VIEW = 'View',
|
||||
}
|
||||
@@ -73,6 +73,7 @@ export * from './Project';
|
||||
export * from './Tasks';
|
||||
export * from './Times';
|
||||
export * from './ProjectProfitabilitySummary';
|
||||
export * from './TaxRate';
|
||||
|
||||
export interface I18nService {
|
||||
__: (input: string) => string;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
|
||||
import { formatNumber } from 'utils';
|
||||
import { isArrayLikeObject } from 'lodash/fp';
|
||||
import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
|
||||
|
||||
export class Transformer {
|
||||
public context: any;
|
||||
@@ -82,6 +81,7 @@ export class Transformer {
|
||||
const normlizedItem = this.normalizeModelItem(item);
|
||||
|
||||
return R.compose(
|
||||
sortObjectKeysAlphabetically,
|
||||
this.transform,
|
||||
R.when(this.hasExcludeAttributes, this.excludeAttributesTransformed),
|
||||
this.includeAttributesTransformed
|
||||
|
||||
@@ -79,6 +79,8 @@ import { ProjectBillableTasksSubscriber } from '@/services/Projects/Projects/Pro
|
||||
import { ProjectBillableExpensesSubscriber } from '@/services/Projects/Projects/ProjectBillableExpenseSubscriber';
|
||||
import { ProjectBillableBillSubscriber } from '@/services/Projects/Projects/ProjectBillableBillSubscriber';
|
||||
import { SyncActualTimeTaskSubscriber } from '@/services/Projects/Times/SyncActualTimeTaskSubscriber';
|
||||
import { SaleInvoiceTaxRateValidateSubscriber } from '@/services/TaxRates/subscribers/SaleInvoiceTaxRateValidateSubscriber';
|
||||
import { WriteInvoiceTaxTransactionsSubscriber } from '@/services/TaxRates/subscribers/WriteInvoiceTaxTransactionsSubscriber';
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -185,5 +187,9 @@ export const susbcribers = () => {
|
||||
ProjectBillableTasksSubscriber,
|
||||
ProjectBillableExpensesSubscriber,
|
||||
ProjectBillableBillSubscriber,
|
||||
|
||||
// Tax Rates
|
||||
SaleInvoiceTaxRateValidateSubscriber,
|
||||
WriteInvoiceTaxTransactionsSubscriber,
|
||||
];
|
||||
};
|
||||
|
||||
@@ -58,6 +58,8 @@ import ItemWarehouseQuantity from 'models/ItemWarehouseQuantity';
|
||||
import Project from 'models/Project';
|
||||
import Time from 'models/Time';
|
||||
import Task from 'models/Task';
|
||||
import TaxRate from 'models/TaxRate';
|
||||
import TaxRateTransaction from 'models/TaxRateTransaction';
|
||||
|
||||
export default (knex) => {
|
||||
const models = {
|
||||
@@ -119,6 +121,8 @@ export default (knex) => {
|
||||
Project,
|
||||
Time,
|
||||
Task,
|
||||
TaxRate,
|
||||
TaxRateTransaction,
|
||||
};
|
||||
return mapValues(models, (model) => model.bindKnex(knex));
|
||||
};
|
||||
|
||||
@@ -6,6 +6,10 @@ import { getTransactionTypeLabel } from '@/utils/transactions-types';
|
||||
|
||||
export default class AccountTransaction extends TenantModel {
|
||||
referenceType: string;
|
||||
credit: number;
|
||||
debit: number;
|
||||
exchangeRate: number;
|
||||
taxRate: number;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
@@ -25,7 +29,23 @@ export default class AccountTransaction extends TenantModel {
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['referenceTypeFormatted'];
|
||||
return ['referenceTypeFormatted', 'creditLocal', 'debitLocal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get creditLocal() {
|
||||
return this.credit * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the debit amount in base currency.
|
||||
* @return {number}
|
||||
*/
|
||||
get debitLocal() {
|
||||
return this.debit * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
|
||||
|
||||
export default class ItemEntry extends TenantModel {
|
||||
public taxRate: number;
|
||||
public discount: number;
|
||||
public quantity: number;
|
||||
public rate: number;
|
||||
public isInclusiveTax: number;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
* @returns {string}
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'items_entries';
|
||||
@@ -11,26 +19,89 @@ export default class ItemEntry extends TenantModel {
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['amount'];
|
||||
return [
|
||||
'amount',
|
||||
'taxAmount',
|
||||
'amountExludingTax',
|
||||
'amountInclusingTax',
|
||||
'total',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry total.
|
||||
* Amount of item entry includes tax and subtracted discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.amountInclusingTax;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount.
|
||||
* Amount of item entry that may include or exclude tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amount() {
|
||||
return ItemEntry.calcAmount(this);
|
||||
return this.quantity * this.rate;
|
||||
}
|
||||
|
||||
static calcAmount(itemEntry) {
|
||||
const { discount, quantity, rate } = itemEntry;
|
||||
const total = quantity * rate;
|
||||
|
||||
return discount ? total - total * discount * 0.01 : total;
|
||||
/**
|
||||
* Item entry amount including tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountInclusingTax() {
|
||||
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry amount excluding tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get amountExludingTax() {
|
||||
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.amount * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag rate fraction.
|
||||
* @returns {number}
|
||||
*/
|
||||
get tagRateFraction() {
|
||||
return this.taxRate / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax amount withheld.
|
||||
* @returns {number}
|
||||
*/
|
||||
get taxAmount() {
|
||||
return this.isInclusiveTax
|
||||
? getInclusiveTaxAmount(this.amount, this.taxRate)
|
||||
: getExlusiveTaxAmount(this.amount, this.taxRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item entry relations.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const Item = require('models/Item');
|
||||
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||
@@ -40,6 +111,7 @@ export default class ItemEntry extends TenantModel {
|
||||
const SaleEstimate = require('models/SaleEstimate');
|
||||
const ProjectTask = require('models/Task');
|
||||
const Expense = require('models/Expense');
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
item: {
|
||||
@@ -86,6 +158,9 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Sale receipt reference.
|
||||
*/
|
||||
receipt: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: SaleReceipt.default,
|
||||
@@ -96,7 +171,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project task reference.
|
||||
*/
|
||||
projectTaskRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -108,7 +183,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project expense reference.
|
||||
*/
|
||||
projectExpenseRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -120,7 +195,7 @@ export default class ItemEntry extends TenantModel {
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Project bill reference.
|
||||
*/
|
||||
projectBillRef: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -130,6 +205,18 @@ export default class ItemEntry extends TenantModel {
|
||||
to: 'bills.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Tax rate reference.
|
||||
*/
|
||||
tax: {
|
||||
relation: Model.HasOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'items_entries.taxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import { castArray } from 'lodash';
|
||||
import { castArray, takeWhile } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSetting from './ModelSetting';
|
||||
@@ -13,6 +13,17 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
CustomViewBaseModel,
|
||||
ModelSearchable,
|
||||
]) {
|
||||
public taxAmountWithheld: number;
|
||||
public balance: number;
|
||||
public paymentAmount: number;
|
||||
public exchangeRate: number;
|
||||
public writtenoffAmount: number;
|
||||
public creditedAmount: number;
|
||||
public isInclusiveTax: boolean;
|
||||
public writtenoffAt: Date;
|
||||
public dueDate: Date;
|
||||
public deliveredAt: Date;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
@@ -27,6 +38,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
get pluralName() {
|
||||
return 'asdfsdf';
|
||||
}
|
||||
@@ -36,35 +50,97 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'dueAmount',
|
||||
'balanceAmount',
|
||||
'isDelivered',
|
||||
'isOverdue',
|
||||
'isPartiallyPaid',
|
||||
'isFullyPaid',
|
||||
'isPaid',
|
||||
'isWrittenoff',
|
||||
'isPaid',
|
||||
|
||||
'dueAmount',
|
||||
'balanceAmount',
|
||||
'remainingDays',
|
||||
'overdueDays',
|
||||
'filterByBranches',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
'subtotalExludingTax',
|
||||
|
||||
'taxAmountWithheldLocal',
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'writtenoffAmountLocal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice amount in local currency.
|
||||
* Invoice amount.
|
||||
* @todo Sugger attribute to balance, we need to rename the balance to amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAmount() {
|
||||
return this.balance * this.exchangeRate;
|
||||
get amount() {
|
||||
return this.balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice local written-off amount.
|
||||
* Invoice amount in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localWrittenoffAmount() {
|
||||
return this.writtenoffAmount * this.exchangeRate;
|
||||
get amountLocal() {
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtotal in base currency. (Tax inclusive) if the tax inclusive is enabled.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.amountLocal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sale invoice amount excluding tax.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalExludingTax() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal - this.taxAmountWithheld
|
||||
: this.subtotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax amount withheld in base currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get taxAmountWithheldLocal() {
|
||||
return this.taxAmountWithheld * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.isInclusiveTax
|
||||
? this.subtotal
|
||||
: this.subtotal + this.taxAmountWithheld;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoice total in local currency. (Tax included)
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,7 +173,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
* @return {boolean}
|
||||
*/
|
||||
get dueAmount() {
|
||||
return Math.max(this.balance - this.balanceAmount, 0);
|
||||
return Math.max(this.total - this.balanceAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +181,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isPartiallyPaid() {
|
||||
return this.dueAmount !== this.balance && this.dueAmount > 0;
|
||||
return this.dueAmount !== this.total && this.dueAmount > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -333,6 +409,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
|
||||
const Branch = require('models/Branch');
|
||||
const Account = require('models/Account');
|
||||
const TaxRateTransaction = require('models/TaxRateTransaction');
|
||||
|
||||
return {
|
||||
/**
|
||||
@@ -382,7 +459,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Invoice may has associated cost transactions.
|
||||
*/
|
||||
costTransactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -397,7 +474,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* Invoice may has associated payment entries.
|
||||
*/
|
||||
paymentEntries: {
|
||||
relation: Model.HasManyRelation,
|
||||
@@ -420,6 +497,9 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated written-off expense account.
|
||||
*/
|
||||
writtenoffExpenseAccount: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Account.default,
|
||||
@@ -428,6 +508,21 @@ export default class SaleInvoice extends mixin(TenantModel, [
|
||||
to: 'accounts.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoice may has associated tax rate transactions.
|
||||
*/
|
||||
taxes: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: TaxRateTransaction.default,
|
||||
join: {
|
||||
from: 'sales_invoices.id',
|
||||
to: 'tax_rate_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'SaleInvoice');
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
48
packages/server/src/models/TaxRate.ts
Normal file
48
packages/server/src/models/TaxRate.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
import SoftDeleteQueryBuilder from '@/collection/SoftDeleteQueryBuilder';
|
||||
|
||||
export default class TaxRate extends mixin(TenantModel, [ModelSearchable]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'tax_rates';
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft delete query builder.
|
||||
*/
|
||||
static get QueryBuilder() {
|
||||
return SoftDeleteQueryBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
56
packages/server/src/models/TaxRateTransaction.ts
Normal file
56
packages/server/src/models/TaxRateTransaction.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import ModelSearchable from './ModelSearchable';
|
||||
|
||||
export default class TaxRateTransaction extends mixin(TenantModel, [
|
||||
ModelSearchable,
|
||||
]) {
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'tax_rate_transactions';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const TaxRate = require('models/TaxRate');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Belongs to the tax rate.
|
||||
*/
|
||||
taxRate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: TaxRate.default,
|
||||
join: {
|
||||
from: 'tax_rate_transactions.taxRateId',
|
||||
to: 'tax_rates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { Account } from 'models';
|
||||
import TenantRepository from '@/repositories/TenantRepository';
|
||||
import { IAccount } from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
|
||||
|
||||
export default class AccountRepository extends TenantRepository {
|
||||
/**
|
||||
@@ -116,7 +117,7 @@ export default class AccountRepository extends TenantRepository {
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
name: this.i18n.__('account.accounts_receivable.currency', {
|
||||
currency: currencyCode
|
||||
currency: currencyCode,
|
||||
}),
|
||||
accountType: 'accounts-receivable',
|
||||
currencyCode,
|
||||
@@ -127,6 +128,29 @@ export default class AccountRepository extends TenantRepository {
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find or create tax payable account.
|
||||
* @param {Record<string, string>}extraAttrs
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns
|
||||
*/
|
||||
async findOrCreateTaxPayable(
|
||||
extraAttrs: Record<string, string> = {},
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
let result = await this.model
|
||||
.query(trx)
|
||||
.findOne({ slug: TaxPayableAccount.slug, ...extraAttrs });
|
||||
|
||||
if (!result) {
|
||||
result = await this.model.query(trx).insertAndFetch({
|
||||
...TaxPayableAccount,
|
||||
...extraAttrs,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
findOrCreateAccountsPayable = async (
|
||||
currencyCode: string = '',
|
||||
extraAttrs = {},
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { castArray, sumBy, toArray } from 'lodash';
|
||||
import { IBill, ISystemUser, IAccount } from '@/interfaces';
|
||||
import { castArray } from 'lodash';
|
||||
import JournalPoster from './JournalPoster';
|
||||
import JournalEntry from './JournalEntry';
|
||||
import { IExpense, IExpenseCategory } from '@/interfaces';
|
||||
import { increment } from 'utils';
|
||||
|
||||
export default class JournalCommands {
|
||||
journal: JournalPoster;
|
||||
models: any;
|
||||
@@ -16,7 +12,6 @@ export default class JournalCommands {
|
||||
*/
|
||||
constructor(journal: JournalPoster) {
|
||||
this.journal = journal;
|
||||
|
||||
this.repositories = this.journal.repositories;
|
||||
this.models = this.journal.models;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,9 @@ export default class Ledger implements ILedger {
|
||||
entryId: entry.id,
|
||||
branchId: entry.branchId,
|
||||
projectId: entry.projectId,
|
||||
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,5 +32,8 @@ export const transformLedgerEntryToTransaction = (
|
||||
projectId: entry.projectId,
|
||||
|
||||
costable: entry.costable,
|
||||
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as R from 'ramda';
|
||||
import { isEmpty, sumBy } from 'lodash';
|
||||
import { ITaxRate } from '@/interfaces';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryPayableById,
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
SalesTaxLiabilitySummaryRate,
|
||||
SalesTaxLiabilitySummaryReportData,
|
||||
SalesTaxLiabilitySummarySalesById,
|
||||
SalesTaxLiabilitySummaryTotal,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import FinancialSheet from '../FinancialSheet';
|
||||
|
||||
export class SalesTaxLiabilitySummary extends FinancialSheet {
|
||||
private query: SalesTaxLiabilitySummaryQuery;
|
||||
private taxRates: ITaxRate[];
|
||||
private payableTaxesById: SalesTaxLiabilitySummaryPayableById;
|
||||
private salesTaxesById: SalesTaxLiabilitySummarySalesById;
|
||||
|
||||
/**
|
||||
* Sales tax liability summary constructor.
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @param {ITaxRate[]} taxRates
|
||||
* @param {SalesTaxLiabilitySummaryPayableById} payableTaxesById
|
||||
* @param {SalesTaxLiabilitySummarySalesById} salesTaxesById
|
||||
*/
|
||||
constructor(
|
||||
query: SalesTaxLiabilitySummaryQuery,
|
||||
taxRates: ITaxRate[],
|
||||
payableTaxesById: SalesTaxLiabilitySummaryPayableById,
|
||||
salesTaxesById: SalesTaxLiabilitySummarySalesById
|
||||
) {
|
||||
super();
|
||||
|
||||
this.query = query;
|
||||
this.taxRates = taxRates;
|
||||
this.payableTaxesById = payableTaxesById;
|
||||
this.salesTaxesById = salesTaxesById;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rate liability node.
|
||||
* @param {ITaxRate} taxRate
|
||||
* @returns {SalesTaxLiabilitySummaryRate}
|
||||
*/
|
||||
private taxRateLiability = (
|
||||
taxRate: ITaxRate
|
||||
): SalesTaxLiabilitySummaryRate => {
|
||||
const payableTax = this.payableTaxesById[taxRate.id];
|
||||
const salesTax = this.salesTaxesById[taxRate.id];
|
||||
|
||||
const payableTaxAmount = payableTax
|
||||
? payableTax.credit - payableTax.debit
|
||||
: 0;
|
||||
const salesTaxAmount = salesTax ? salesTax.credit - salesTax.debit : 0;
|
||||
|
||||
// Calculates the tax percentage.
|
||||
const taxPercentage = R.compose(
|
||||
R.unless(R.equals(0), R.divide(R.__, salesTaxAmount))
|
||||
)(payableTaxAmount);
|
||||
|
||||
// Calculates the payable tax amount.
|
||||
const collectedTaxAmount = payableTax ? payableTax.debit : 0;
|
||||
|
||||
return {
|
||||
id: taxRate.id,
|
||||
taxName: `${taxRate.name} (${taxRate.rate}%)`,
|
||||
taxableAmount: this.getAmountMeta(salesTaxAmount),
|
||||
taxAmount: this.getAmountMeta(payableTaxAmount),
|
||||
taxPercentage: this.getPercentageTotalAmountMeta(taxPercentage),
|
||||
collectedTaxAmount: this.getAmountMeta(collectedTaxAmount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the non-transactions tax rates.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||
*/
|
||||
private filterNonTransactionsTaxRates = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): SalesTaxLiabilitySummaryRate[] => {
|
||||
return nodes.filter((node) => {
|
||||
const salesTrxs = this.salesTaxesById[node.id];
|
||||
const payableTrxs = this.payableTaxesById[node.id];
|
||||
|
||||
return !isEmpty(salesTrxs) || !isEmpty(payableTrxs);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates liability nodes.
|
||||
* @returns {SalesTaxLiabilitySummaryRate[]}
|
||||
*/
|
||||
private taxRatesLiability = (): SalesTaxLiabilitySummaryRate[] => {
|
||||
return R.compose(
|
||||
this.filterNonTransactionsTaxRates,
|
||||
R.map(this.taxRateLiability)
|
||||
)(this.taxRates);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates total node.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {SalesTaxLiabilitySummaryTotal}
|
||||
*/
|
||||
private taxRatesTotal = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): SalesTaxLiabilitySummaryTotal => {
|
||||
const taxableAmount = sumBy(nodes, 'taxableAmount.amount');
|
||||
const taxAmount = sumBy(nodes, 'taxAmount.amount');
|
||||
const collectedTaxAmount = sumBy(nodes, 'collectedTaxAmount.amount');
|
||||
|
||||
return {
|
||||
taxableAmount: this.getTotalAmountMeta(taxableAmount),
|
||||
taxAmount: this.getTotalAmountMeta(taxAmount),
|
||||
collectedTaxAmount: this.getTotalAmountMeta(collectedTaxAmount),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the report data.
|
||||
* @returns {SalesTaxLiabilitySummaryReportData}
|
||||
*/
|
||||
public reportData = (): SalesTaxLiabilitySummaryReportData => {
|
||||
const taxRates = this.taxRatesLiability();
|
||||
const total = this.taxRatesTotal(taxRates);
|
||||
|
||||
return { taxRates, total };
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryPayableById,
|
||||
SalesTaxLiabilitySummarySalesById,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { keyBy } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export class SalesTaxLiabilitySummaryRepository {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve tax rates.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<TaxRate[]>}
|
||||
*/
|
||||
public taxRates = (tenantId: number) => {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
return TaxRate.query().orderBy('name', 'desc');
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve taxes payable sum grouped by tax rate id.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<SalesTaxLiabilitySummaryPayableById>}
|
||||
*/
|
||||
public async taxesPayableSumGroupedByRateId(
|
||||
tenantId: number
|
||||
): Promise<SalesTaxLiabilitySummaryPayableById> {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves tax payable accounts.
|
||||
const taxPayableAccounts = await Account.query().whereIn('accountType', [
|
||||
ACCOUNT_TYPE.TAX_PAYABLE,
|
||||
]);
|
||||
const payableAccountsIds = taxPayableAccounts.map((account) => account.id);
|
||||
|
||||
const groupedTaxesById = await AccountTransaction.query()
|
||||
.whereIn('account_id', payableAccountsIds)
|
||||
.whereNot('tax_rate_id', null)
|
||||
.groupBy('tax_rate_id')
|
||||
.select(['tax_rate_id'])
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit');
|
||||
|
||||
return keyBy(groupedTaxesById, 'taxRateId');
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve taxes sales sum grouped by tax rate id.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<SalesTaxLiabilitySummarySalesById>}
|
||||
*/
|
||||
public taxesSalesSumGroupedByRateId = async (
|
||||
tenantId: number
|
||||
): Promise<SalesTaxLiabilitySummarySalesById> => {
|
||||
const { AccountTransaction, Account } = this.tenancy.models(tenantId);
|
||||
|
||||
const incomeAccounts = await Account.query().whereIn('accountType', [
|
||||
ACCOUNT_TYPE.INCOME,
|
||||
ACCOUNT_TYPE.OTHER_INCOME,
|
||||
]);
|
||||
const incomeAccountsIds = incomeAccounts.map((account) => account.id);
|
||||
|
||||
const groupedTaxesById = await AccountTransaction.query()
|
||||
.whereIn('account_id', incomeAccountsIds)
|
||||
.whereNot('tax_rate_id', null)
|
||||
.groupBy('tax_rate_id')
|
||||
.select(['tax_rate_id'])
|
||||
.sum('credit as credit')
|
||||
.sum('debit as debit');
|
||||
|
||||
return keyBy(groupedTaxesById, 'taxRateId');
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { SalesTaxLiabilitySummaryRepository } from './SalesTaxLiabilitySummaryRepository';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryMeta,
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import { SalesTaxLiabilitySummary } from './SalesTaxLiabilitySummary';
|
||||
import { SalesTaxLiabilitySummaryTable } from './SalesTaxLiabilitySummaryTable';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class SalesTaxLiabilitySummaryService {
|
||||
@Inject()
|
||||
private repostiory: SalesTaxLiabilitySummaryRepository;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Retrieve sales tax liability summary.
|
||||
* @param {number} tenantId
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @returns
|
||||
*/
|
||||
public async salesTaxLiability(
|
||||
tenantId: number,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
const payableByRateId =
|
||||
await this.repostiory.taxesPayableSumGroupedByRateId(tenantId);
|
||||
|
||||
const salesByRateId = await this.repostiory.taxesSalesSumGroupedByRateId(
|
||||
tenantId
|
||||
);
|
||||
const taxRates = await this.repostiory.taxRates(tenantId);
|
||||
|
||||
const taxLiabilitySummary = new SalesTaxLiabilitySummary(
|
||||
query,
|
||||
taxRates,
|
||||
payableByRateId,
|
||||
salesByRateId
|
||||
);
|
||||
return {
|
||||
data: taxLiabilitySummary.reportData(),
|
||||
query,
|
||||
meta: this.reportMetadata(tenantId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve sales tax liability summary table.
|
||||
* @param {number} tenantId
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
* @returns
|
||||
*/
|
||||
public async salesTaxLiabilitySummaryTable(
|
||||
tenantId: number,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
const report = await this.salesTaxLiability(tenantId, query);
|
||||
|
||||
// Creates the sales tax liability summary table.
|
||||
const table = new SalesTaxLiabilitySummaryTable(report.data, query);
|
||||
|
||||
return {
|
||||
table: {
|
||||
rows: table.tableRows(),
|
||||
columns: table.tableColumns(),
|
||||
},
|
||||
data: report.data,
|
||||
query: report.query,
|
||||
meta: report.meta,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the report meta.
|
||||
* @param {number} tenantId -
|
||||
* @returns {IBalanceSheetMeta}
|
||||
*/
|
||||
private reportMetadata(tenantId: number): SalesTaxLiabilitySummaryMeta {
|
||||
const settings = this.tenancy.settings(tenantId);
|
||||
|
||||
const organizationName = settings.get({
|
||||
group: 'organization',
|
||||
key: 'name',
|
||||
});
|
||||
const baseCurrency = settings.get({
|
||||
group: 'organization',
|
||||
key: 'base_currency',
|
||||
});
|
||||
|
||||
return {
|
||||
organizationName,
|
||||
baseCurrency,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import * as R from 'ramda';
|
||||
import {
|
||||
SalesTaxLiabilitySummaryQuery,
|
||||
SalesTaxLiabilitySummaryRate,
|
||||
SalesTaxLiabilitySummaryReportData,
|
||||
SalesTaxLiabilitySummaryTotal,
|
||||
} from '@/interfaces/SalesTaxLiabilitySummary';
|
||||
import { tableRowMapper } from '@/utils';
|
||||
import { ITableColumn, ITableRow } from '@/interfaces';
|
||||
import { FinancialSheetStructure } from '../FinancialSheetStructure';
|
||||
import { FinancialTable } from '../FinancialTable';
|
||||
import AgingReport from '../AgingSummary/AgingReport';
|
||||
import { IROW_TYPE } from './_constants';
|
||||
|
||||
export class SalesTaxLiabilitySummaryTable extends R.compose(
|
||||
FinancialSheetStructure,
|
||||
FinancialTable
|
||||
)(AgingReport) {
|
||||
private data: SalesTaxLiabilitySummaryReportData;
|
||||
private query: SalesTaxLiabilitySummaryQuery;
|
||||
|
||||
/**
|
||||
* Sales tax liability summary table constructor.
|
||||
* @param {SalesTaxLiabilitySummaryReportData} data
|
||||
* @param {SalesTaxLiabilitySummaryQuery} query
|
||||
*/
|
||||
constructor(
|
||||
data: SalesTaxLiabilitySummaryReportData,
|
||||
query: SalesTaxLiabilitySummaryQuery
|
||||
) {
|
||||
super();
|
||||
|
||||
this.data = data;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tax rate row accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
private get taxRateRowAccessor() {
|
||||
return [
|
||||
{ key: 'taxName', accessor: 'taxName' },
|
||||
{ key: 'taxPercentage', accessor: 'taxPercentage.formattedAmount' },
|
||||
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the tax rate total row accessors.
|
||||
* @returns {ITableColumnAccessor[]}
|
||||
*/
|
||||
private get taxRateTotalRowAccessors() {
|
||||
return [
|
||||
{ key: 'taxName', value: 'Total' },
|
||||
{ key: 'taxPercentage', value: '' },
|
||||
{ key: 'taxableAmount', accessor: 'taxableAmount.formattedAmount' },
|
||||
{ key: 'collectedTax', accessor: 'collectedTaxAmount.formattedAmount' },
|
||||
{ key: 'taxAmount', accessor: 'taxAmount.formattedAmount' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the tax rate node to table row.
|
||||
* @param {SalesTaxLiabilitySummaryRate} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private taxRateTableRowMapper = (
|
||||
node: SalesTaxLiabilitySummaryRate
|
||||
): ITableRow => {
|
||||
const columns = this.taxRateRowAccessor;
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.TaxRate],
|
||||
id: node.id,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the tax rates nodes to table rows.
|
||||
* @param {SalesTaxLiabilitySummaryRate[]} nodes
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private taxRatesTableRowsMapper = (
|
||||
nodes: SalesTaxLiabilitySummaryRate[]
|
||||
): ITableRow[] => {
|
||||
return nodes.map(this.taxRateTableRowMapper);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps the tax rate total node to table row.
|
||||
* @param {SalesTaxLiabilitySummaryTotal} node
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private taxRateTotalRowMapper = (node: SalesTaxLiabilitySummaryTotal) => {
|
||||
const columns = this.taxRateTotalRowAccessors;
|
||||
const meta = {
|
||||
rowTypes: [IROW_TYPE.Total],
|
||||
id: node.key,
|
||||
};
|
||||
return tableRowMapper(node, columns, meta);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the tax rate total row.
|
||||
* @returns {ITableRow}
|
||||
*/
|
||||
private get taxRateTotalRow(): ITableRow {
|
||||
return this.taxRateTotalRowMapper(this.data.total);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
private get taxRatesRows(): ITableRow[] {
|
||||
return this.taxRatesTableRowsMapper(this.data.taxRates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table rows.
|
||||
* @returns {ITableRow[]}
|
||||
*/
|
||||
public tableRows(): ITableRow[] {
|
||||
return R.compose(
|
||||
R.unless(R.isEmpty, R.append(this.taxRateTotalRow)),
|
||||
R.concat(this.taxRatesRows)
|
||||
)([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the table columns.
|
||||
* @returns {ITableColumn[]}
|
||||
*/
|
||||
public tableColumns(): ITableColumn[] {
|
||||
return R.compose(this.tableColumnsCellIndexing)([
|
||||
{
|
||||
label: 'Tax Name',
|
||||
key: 'taxName',
|
||||
},
|
||||
{
|
||||
label: 'Tax Percentage',
|
||||
key: 'taxPercentage',
|
||||
},
|
||||
{
|
||||
label: 'Taxable Amount',
|
||||
key: 'taxableAmount',
|
||||
},
|
||||
{
|
||||
label: 'Collected Tax',
|
||||
key: 'collectedTax',
|
||||
},
|
||||
{
|
||||
label: 'Tax Amount',
|
||||
key: 'taxRate',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum IROW_TYPE {
|
||||
TaxRate = 'TaxRate',
|
||||
Total = 'Total',
|
||||
}
|
||||
@@ -264,4 +264,13 @@ export default class ItemsEntriesService {
|
||||
public getTotalItemsEntries(entries: ItemEntry[]): number {
|
||||
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the non-zero tax items entries.
|
||||
* @param {IItemEntry[]} entries -
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
public getNonZeroEntries(entries: IItemEntry[]): IItemEntry[] {
|
||||
return entries.filter((e) => e.taxRate > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,16 +13,14 @@ import {
|
||||
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
|
||||
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
|
||||
import { formatDateFields } from 'utils';
|
||||
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
|
||||
import { ItemEntry } from '@/models';
|
||||
|
||||
@Service()
|
||||
export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private branchDTOTransform: BranchTransactionDTOTransform;
|
||||
|
||||
@@ -38,6 +36,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
@Inject()
|
||||
private invoiceIncrement: SaleInvoiceIncrement;
|
||||
|
||||
@Inject()
|
||||
private taxDTOTransformer: ItemEntriesTaxTransactions;
|
||||
|
||||
/**
|
||||
* Transformes the create DTO to invoice object model.
|
||||
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
|
||||
@@ -51,11 +52,9 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
authorizedUser: ITenantUser,
|
||||
oldSaleInvoice?: ISaleInvoice
|
||||
): Promise<ISaleInvoice> {
|
||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
||||
const entriesModels = this.transformDTOEntriesToModels(saleInvoiceDTO);
|
||||
const amount = this.getDueBalanceItemEntries(entriesModels);
|
||||
|
||||
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
|
||||
ItemEntry.calcAmount(e)
|
||||
);
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber = this.invoiceIncrement.getNextInvoiceNumber(tenantId);
|
||||
|
||||
@@ -68,20 +67,30 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
|
||||
const initialEntries = saleInvoiceDTO.entries.map((entry) => ({
|
||||
referenceType: 'SaleInvoice',
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
...entry,
|
||||
}));
|
||||
const entries = await composeAsync(
|
||||
const asyncEntries = await composeAsync(
|
||||
// Associate tax rate from tax id to entries.
|
||||
this.taxDTOTransformer.assocTaxRateFromTaxIdToEntries(tenantId),
|
||||
// Associate tax rate id from tax code to entries.
|
||||
this.taxDTOTransformer.assocTaxRateIdFromCodeToEntries(tenantId),
|
||||
// Sets default cost and sell account to invoice items entries.
|
||||
this.itemsEntriesService.setItemsEntriesDefaultAccounts(tenantId)
|
||||
)(initialEntries);
|
||||
|
||||
const entries = R.compose(
|
||||
// Remove tax code from entries.
|
||||
R.map(R.omit(['taxCode']))
|
||||
)(asyncEntries);
|
||||
|
||||
const initialDTO = {
|
||||
...formatDateFields(
|
||||
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
|
||||
['invoiceDate', 'dueDate']
|
||||
),
|
||||
// Avoid rewrite the deliver date in edit mode when already published.
|
||||
balance,
|
||||
balance: amount,
|
||||
currencyCode: customer.currencyCode,
|
||||
exchangeRate: saleInvoiceDTO.exchangeRate || 1,
|
||||
...(saleInvoiceDTO.delivered &&
|
||||
@@ -96,8 +105,34 @@ export class CommandSaleInvoiceDTOTransformer {
|
||||
} as ISaleInvoice;
|
||||
|
||||
return R.compose(
|
||||
this.taxDTOTransformer.assocTaxAmountWithheldFromEntries,
|
||||
this.branchDTOTransform.transformDTO<ISaleInvoice>(tenantId),
|
||||
this.warehouseDTOTransform.transformDTO<ISaleInvoice>(tenantId)
|
||||
)(initialDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the DTO entries to invoice entries models.
|
||||
* @param {ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO} entries
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private transformDTOEntriesToModels = (
|
||||
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO
|
||||
): ItemEntry[] => {
|
||||
return saleInvoiceDTO.entries.map((entry) => {
|
||||
return ItemEntry.fromJson({
|
||||
...entry,
|
||||
isInclusiveTax: saleInvoiceDTO.isInclusiveTax,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the due balance from the invoice entries.
|
||||
* @param {IItemEntry[]} entries
|
||||
* @returns {number}
|
||||
*/
|
||||
private getDueBalanceItemEntries = (entries: ItemEntry[]) => {
|
||||
return sumBy(entries, (e) => e.amount);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ export class GetSaleInvoice {
|
||||
const saleInvoice = await SaleInvoice.query()
|
||||
.findById(saleInvoiceId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('entries.tax')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch');
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('taxes.taxRate');
|
||||
|
||||
// Validates the given sale invoice existance.
|
||||
this.validators.validateInvoiceExistance(saleInvoice);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ISaleInvoice,
|
||||
IItemEntry,
|
||||
@@ -6,11 +7,11 @@ import {
|
||||
AccountNormal,
|
||||
ILedger,
|
||||
} from '@/interfaces';
|
||||
import { Knex } from 'knex';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import Ledger from '@/services/Accounting/Ledger';
|
||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceGLEntries {
|
||||
@@ -20,10 +21,13 @@ export class SaleInvoiceGLEntries {
|
||||
@Inject()
|
||||
private ledegrRepository: LedgerStorageService;
|
||||
|
||||
@Inject()
|
||||
private itemsEntriesService: ItemsEntriesService;
|
||||
|
||||
/**
|
||||
* Writes a sale invoice GL entries.
|
||||
* @param {number} tenantId
|
||||
* @param {number} saleInvoiceId
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} saleInvoiceId - Sale invoice id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public writeInvoiceGLEntries = async (
|
||||
@@ -42,9 +46,17 @@ export class SaleInvoiceGLEntries {
|
||||
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
|
||||
saleInvoice.currencyCode
|
||||
);
|
||||
// Find or create tax payable account.
|
||||
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
|
||||
{},
|
||||
trx
|
||||
);
|
||||
// Retrieves the ledger of the invoice.
|
||||
const ledger = this.getInvoiceGLedger(saleInvoice, ARAccount.id);
|
||||
|
||||
const ledger = this.getInvoiceGLedger(
|
||||
saleInvoice,
|
||||
ARAccount.id,
|
||||
taxPayableAccount.id
|
||||
);
|
||||
// Commits the ledger entries to the storage as UOW.
|
||||
await this.ledegrRepository.commit(tenantId, ledger, trx);
|
||||
};
|
||||
@@ -94,10 +106,14 @@ export class SaleInvoiceGLEntries {
|
||||
*/
|
||||
public getInvoiceGLedger = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
): ILedger => {
|
||||
const entries = this.getInvoiceGLEntries(saleInvoice, ARAccountId);
|
||||
|
||||
const entries = this.getInvoiceGLEntries(
|
||||
saleInvoice,
|
||||
ARAccountId,
|
||||
taxPayableAccountId
|
||||
);
|
||||
return new Ledger(entries);
|
||||
};
|
||||
|
||||
@@ -143,7 +159,7 @@ export class SaleInvoiceGLEntries {
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: saleInvoice.localAmount,
|
||||
debit: saleInvoice.totalLocal,
|
||||
accountId: ARAccountId,
|
||||
contactId: saleInvoice.customerId,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
@@ -165,7 +181,7 @@ export class SaleInvoiceGLEntries {
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
const localAmount = entry.amount * saleInvoice.exchangeRate;
|
||||
const localAmount = entry.amountExludingTax * saleInvoice.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
@@ -176,11 +192,62 @@ export class SaleInvoiceGLEntries {
|
||||
itemId: entry.itemId,
|
||||
itemQuantity: entry.quantity,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
projectId: entry.projectId || saleInvoice.projectId
|
||||
projectId: entry.projectId || saleInvoice.projectId,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retreives the GL entry of tax payable.
|
||||
* @param {ISaleInvoice} saleInvoice -
|
||||
* @param {number} taxPayableAccountId -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getInvoiceTaxEntry = R.curry(
|
||||
(
|
||||
saleInvoice: ISaleInvoice,
|
||||
taxPayableAccountId: number,
|
||||
entry: IItemEntry,
|
||||
index: number
|
||||
): ILedgerEntry => {
|
||||
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: entry.taxAmount,
|
||||
accountId: taxPayableAccountId,
|
||||
index: index + 3,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
taxRateId: entry.taxRateId,
|
||||
taxRate: entry.taxRate,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the invoice tax GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
* @param {number} taxPayableAccountId
|
||||
* @returns {ILedgerEntry[]}
|
||||
*/
|
||||
private getInvoiceTaxEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
taxPayableAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
// Retrieves the non-zero tax entries.
|
||||
const nonZeroTaxEntries = this.itemsEntriesService.getNonZeroEntries(
|
||||
saleInvoice.entries
|
||||
);
|
||||
const transformTaxEntry = this.getInvoiceTaxEntry(
|
||||
saleInvoice,
|
||||
taxPayableAccountId
|
||||
);
|
||||
// Transforms the non-zero tax entries to GL entries.
|
||||
return nonZeroTaxEntries.map(transformTaxEntry);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the invoice GL entries.
|
||||
* @param {ISaleInvoice} saleInvoice
|
||||
@@ -189,7 +256,8 @@ export class SaleInvoiceGLEntries {
|
||||
*/
|
||||
public getInvoiceGLEntries = (
|
||||
saleInvoice: ISaleInvoice,
|
||||
ARAccountId: number
|
||||
ARAccountId: number,
|
||||
taxPayableAccountId: number
|
||||
): ILedgerEntry[] => {
|
||||
const receivableEntry = this.getInvoiceReceivableEntry(
|
||||
saleInvoice,
|
||||
@@ -198,6 +266,10 @@ export class SaleInvoiceGLEntries {
|
||||
const transformItemEntry = this.getInvoiceItemEntry(saleInvoice);
|
||||
const creditEntries = saleInvoice.entries.map(transformItemEntry);
|
||||
|
||||
return [receivableEntry, ...creditEntries];
|
||||
const taxEntries = this.getInvoiceTaxEntries(
|
||||
saleInvoice,
|
||||
taxPayableAccountId
|
||||
);
|
||||
return [receivableEntry, ...creditEntries, ...taxEntries];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from '@/utils';
|
||||
import { getExlusiveTaxAmount, getInclusiveTaxAmount } from '@/utils/taxRate';
|
||||
import { format } from 'mathjs';
|
||||
|
||||
export class SaleInvoiceTaxEntryTransformer extends Transformer {
|
||||
/**
|
||||
* Included attributes.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'name',
|
||||
'taxRateCode',
|
||||
'taxRate',
|
||||
'taxRateId',
|
||||
'taxRateAmount',
|
||||
'taxRateAmountFormatted',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Exclude attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['*'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate code.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateCode = (taxEntry) => {
|
||||
return taxEntry.taxRate.code;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate id.
|
||||
* @param taxEntry
|
||||
* @returns {number}
|
||||
*/
|
||||
protected taxRate = (taxEntry) => {
|
||||
return taxEntry.taxAmount || taxEntry.taxRate.rate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate name.
|
||||
* @param taxEntry
|
||||
* @returns {string}
|
||||
*/
|
||||
protected name = (taxEntry) => {
|
||||
return taxEntry.taxRate.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve tax rate amount.
|
||||
* @param taxEntry
|
||||
*/
|
||||
protected taxRateAmount = (taxEntry) => {
|
||||
const taxRate = this.taxRate(taxEntry);
|
||||
|
||||
return this.options.isInclusiveTax
|
||||
? getInclusiveTaxAmount(this.options.amount, taxRate)
|
||||
: getExlusiveTaxAmount(this.options.amount, taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted tax rate amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxRateAmountFormatted = (taxEntry) => {
|
||||
return formatNumber(this.taxRateAmount(taxEntry), {
|
||||
currencyCode: this.options.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
import { formatNumber } from 'utils';
|
||||
import { SaleInvoiceTaxEntryTransformer } from './SaleInvoiceTaxEntryTransformer';
|
||||
|
||||
export class SaleInvoiceTransformer extends Transformer {
|
||||
/**
|
||||
@@ -8,13 +9,20 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedInvoiceDate',
|
||||
'formattedDueDate',
|
||||
'formattedAmount',
|
||||
'formattedDueAmount',
|
||||
'formattedPaymentAmount',
|
||||
'formattedBalanceAmount',
|
||||
'formattedExchangeRate',
|
||||
'invoiceDateFormatted',
|
||||
'dueDateFormatted',
|
||||
'dueAmountFormatted',
|
||||
'paymentAmountFormatted',
|
||||
'balanceAmountFormatted',
|
||||
'exchangeRateFormatted',
|
||||
'subtotalFormatted',
|
||||
'subtotalLocalFormatted',
|
||||
'subtotalExludingTaxFormatted',
|
||||
'taxAmountWithheldFormatted',
|
||||
'taxAmountWithheldLocalFormatted',
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
'taxes',
|
||||
];
|
||||
};
|
||||
|
||||
@@ -23,7 +31,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedInvoiceDate = (invoice): string => {
|
||||
protected invoiceDateFormatted = (invoice): string => {
|
||||
return this.formatDate(invoice.invoiceDate);
|
||||
};
|
||||
|
||||
@@ -32,27 +40,16 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueDate = (invoice): string => {
|
||||
protected dueDateFormatted = (invoice): string => {
|
||||
return this.formatDate(invoice.dueDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (invoice): string => {
|
||||
return formatNumber(invoice.balance, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice due amount.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDueAmount = (invoice): string => {
|
||||
protected dueAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.dueAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -63,7 +60,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedPaymentAmount = (invoice): string => {
|
||||
protected paymentAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.paymentAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -74,7 +71,7 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedBalanceAmount = (invoice): string => {
|
||||
protected balanceAmountFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.balanceAmount, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
@@ -85,7 +82,98 @@ export class SaleInvoiceTransformer extends Transformer {
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedExchangeRate = (invoice): string => {
|
||||
protected exchangeRateFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.exchangeRate, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in base currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal in foreign currency.
|
||||
* (Tax inclusive if the tax inclusive is enabled)
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotalLocal, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted subtotal excluding tax in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected subtotalExludingTaxFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.subtotalExludingTax, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.taxAmountWithheld, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted tax amount withheld in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected taxAmountWithheldLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.taxAmountWithheldLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in foreign currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.total, {
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted total in base currency.
|
||||
* @param invoice
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (invoice): string => {
|
||||
return formatNumber(invoice.totalLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the taxes lines of sale invoice.
|
||||
* @param {ISaleInvoice} invoice
|
||||
*/
|
||||
protected taxes = (invoice) => {
|
||||
return this.item(invoice.taxes, new SaleInvoiceTaxEntryTransformer(), {
|
||||
amount: invoice.amount,
|
||||
isInclusiveTax: invoice.isInclusiveTax,
|
||||
currencyCode: invoice.currencyCode,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ export class PaymentReceivesApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
* deletes the given payment receive.
|
||||
* Deletes the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @param {ISystemUser} authorizedUser
|
||||
@@ -126,7 +126,7 @@ export class PaymentReceivesApplication {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Retrieves the given payment receive.
|
||||
* @param {number} tenantId
|
||||
* @param {number} paymentReceiveId
|
||||
* @returns {Promise<IPaymentReceive>}
|
||||
|
||||
67
packages/server/src/services/TaxRates/ActivateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/ActivateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ITaxRateActivatedPayload,
|
||||
ITaxRateActivatingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class ActivateTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Activates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public activateTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
// Validates the tax rate inactive.
|
||||
this.validators.validateTaxRateNotActive(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateActivating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onActivating, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx)
|
||||
.findById(taxRateId)
|
||||
.patch({ active: 1 });
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onActivated, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { IItemEntryDTO, ITaxRate } from '@/interfaces';
|
||||
import { ERRORS } from './constants';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
@Service()
|
||||
export class CommandTaxRatesValidators {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validates the tax rate existance.
|
||||
* @param {TaxRate | undefined | null} taxRate
|
||||
*/
|
||||
public validateTaxRateExistance(taxRate: ITaxRate | undefined | null) {
|
||||
if (!taxRate) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given tax rate active.
|
||||
* @param {ITaxRate} taxRate
|
||||
*/
|
||||
public validateTaxRateNotActive(taxRate: ITaxRate) {
|
||||
if (taxRate.active) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_ALREADY_ACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given tax rate inactive.
|
||||
* @param {ITaxRate} taxRate
|
||||
*/
|
||||
public validateTaxRateNotInactive(taxRate: ITaxRate) {
|
||||
if (!taxRate.active) {
|
||||
throw new ServiceError(ERRORS.TAX_RATE_ALREADY_INACTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax code uniquiness.
|
||||
* @param {number} tenantId
|
||||
* @param {string} taxCode
|
||||
*/
|
||||
public async validateTaxCodeUnique(tenantId: number, taxCode: string) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundTaxCode = await TaxRate.query().findOne({ code: taxCode });
|
||||
|
||||
if (foundTaxCode) {
|
||||
throw new ServiceError(ERRORS.TAX_CODE_NOT_UNIQUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax codes of the given item entries DTO.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateItemEntriesTaxCode(
|
||||
tenantId: number,
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxCode);
|
||||
const taxCodes = filteredTaxEntries.map((e) => e.taxCode);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxCodes.length === 0) return;
|
||||
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
const foundCodes = foundTaxCodes.map((tax) => tax.code);
|
||||
|
||||
const notFoundTaxCodes = difference(taxCodes, foundCodes);
|
||||
|
||||
if (notFoundTaxCodes.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the tax rate id of the given item entries DTO.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntryDTO[]} itemEntriesDTO
|
||||
* @throws {ServiceError}
|
||||
*/
|
||||
public async validateItemEntriesTaxCodeId(
|
||||
tenantId: number,
|
||||
itemEntriesDTO: IItemEntryDTO[]
|
||||
) {
|
||||
const filteredTaxEntries = itemEntriesDTO.filter((e) => e.taxRateId);
|
||||
const taxRatesIds = filteredTaxEntries.map((e) => e.taxRateId);
|
||||
|
||||
// Can't validate if there is no tax codes.
|
||||
if (taxRatesIds.length === 0) return;
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('id', taxRatesIds);
|
||||
const foundTaxRatesIds = foundTaxCodes.map((tax) => tax.id);
|
||||
|
||||
const notFoundTaxCodes = difference(taxRatesIds, foundTaxRatesIds);
|
||||
|
||||
if (notFoundTaxCodes.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/TaxRates/CreateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/CreateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ICreateTaxRateDTO,
|
||||
ITaxRateCreatedPayload,
|
||||
ITaxRateCreatingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
|
||||
@Service()
|
||||
export class CreateTaxRate {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreateTaxRateDTO} createTaxRateDTO
|
||||
*/
|
||||
public async createTaxRate(
|
||||
tenantId: number,
|
||||
createTaxRateDTO: ICreateTaxRateDTO
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Validates the tax code uniquiness.
|
||||
await this.validators.validateTaxCodeUnique(
|
||||
tenantId,
|
||||
createTaxRateDTO.code
|
||||
);
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onCreating, {
|
||||
createTaxRateDTO,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateCreatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx).insertAndFetch({
|
||||
...createTaxRateDTO,
|
||||
});
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onCreated, {
|
||||
createTaxRateDTO,
|
||||
taxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateCreatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
56
packages/server/src/services/TaxRates/DeleteTaxRate.ts
Normal file
56
packages/server/src/services/TaxRates/DeleteTaxRate.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { ITaxRateDeletedPayload, ITaxRateDeletingPayload } from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class DeleteTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onDeleting, {
|
||||
oldTaxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateDeletingPayload);
|
||||
|
||||
await TaxRate.query(trx).findById(taxRateId).delete();
|
||||
|
||||
// Triggers `onTaxRateDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onDeleted, {
|
||||
oldTaxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateDeletedPayload);
|
||||
});
|
||||
}
|
||||
}
|
||||
126
packages/server/src/services/TaxRates/EditTaxRate.ts
Normal file
126
packages/server/src/services/TaxRates/EditTaxRate.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { omit } from 'lodash';
|
||||
import {
|
||||
IEditTaxRateDTO,
|
||||
ITaxRate,
|
||||
ITaxRateEditedPayload,
|
||||
ITaxRateEditingPayload,
|
||||
} from '@/interfaces';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class EditTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Detarmines whether the tax rate, name or code have been changed.
|
||||
* @param {ITaxRate} taxRate
|
||||
* @param {IEditTaxRateDTO} editTaxRateDTO
|
||||
* @returns {boolean}
|
||||
*/
|
||||
private isTaxRateDTOChanged = (
|
||||
taxRate: ITaxRate,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) => {
|
||||
return (
|
||||
taxRate.rate !== editTaxRateDTO.rate ||
|
||||
taxRate.name !== editTaxRateDTO.name ||
|
||||
taxRate.code !== editTaxRateDTO.code
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits the given tax rate or creates a new if the rate or name have been changed.
|
||||
* @param {number} tenantId
|
||||
* @param {ITaxRate} oldTaxRate
|
||||
* @param {IEditTaxRateDTO} editTaxRateDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
private async editTaxRateOrCreate(
|
||||
tenantId: number,
|
||||
oldTaxRate: ITaxRate,
|
||||
editTaxRateDTO: IEditTaxRateDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const isTaxDTOChanged = this.isTaxRateDTOChanged(
|
||||
oldTaxRate,
|
||||
editTaxRateDTO
|
||||
);
|
||||
if (isTaxDTOChanged) {
|
||||
// Soft deleting the old tax rate.
|
||||
await TaxRate.query(trx).findById(oldTaxRate.id).delete();
|
||||
|
||||
// Create a new tax rate with new edited data.
|
||||
return TaxRate.query(trx).insertAndFetch({
|
||||
...omit(oldTaxRate, ['id']),
|
||||
...editTaxRateDTO,
|
||||
});
|
||||
} else {
|
||||
return TaxRate.query(trx).patchAndFetchById(oldTaxRate.id, {
|
||||
...editTaxRateDTO,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async editTaxRate(
|
||||
tenantId: number,
|
||||
taxRateId: number,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onEditing, {
|
||||
editTaxRateDTO,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateEditingPayload);
|
||||
|
||||
const taxRate = await this.editTaxRateOrCreate(
|
||||
tenantId,
|
||||
oldTaxRate,
|
||||
editTaxRateDTO,
|
||||
trx
|
||||
);
|
||||
// Triggers `onTaxRateEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onEdited, {
|
||||
editTaxRateDTO,
|
||||
taxRate,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateEditedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
39
packages/server/src/services/TaxRates/GetTaxRate.ts
Normal file
39
packages/server/src/services/TaxRates/GetTaxRate.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { TaxRateTransformer } from './TaxRateTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async getTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const taxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(taxRate);
|
||||
|
||||
// Transforms the tax rate.
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
taxRate,
|
||||
new TaxRateTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
32
packages/server/src/services/TaxRates/GetTaxRates.ts
Normal file
32
packages/server/src/services/TaxRates/GetTaxRates.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { TaxRateTransformer } from './TaxRateTransformer';
|
||||
|
||||
@Service()
|
||||
export class GetTaxRatesService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ITaxRate[]>}
|
||||
*/
|
||||
public async getTaxRates(tenantId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
// Retrieves the tax rates.
|
||||
const taxRates = await TaxRate.query().orderBy('name', 'ASC');
|
||||
|
||||
// Transforms the tax rates.
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
taxRates,
|
||||
new TaxRateTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
67
packages/server/src/services/TaxRates/InactivateTaxRate.ts
Normal file
67
packages/server/src/services/TaxRates/InactivateTaxRate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
ITaxRateActivatedPayload,
|
||||
ITaxRateActivatingPayload,
|
||||
} from '@/interfaces';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Knex } from 'knex';
|
||||
import { CommandTaxRatesValidators } from './CommandTaxRatesValidators';
|
||||
import events from '@/subscribers/events';
|
||||
|
||||
@Service()
|
||||
export class InactivateTaxRateService {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
@Inject()
|
||||
private validators: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public async inactivateTaxRate(tenantId: number, taxRateId: number) {
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
|
||||
const oldTaxRate = await TaxRate.query().findById(taxRateId);
|
||||
|
||||
// Validates the tax rate existance.
|
||||
this.validators.validateTaxRateExistance(oldTaxRate);
|
||||
|
||||
// Validates the tax rate active.
|
||||
this.validators.validateTaxRateNotInactive(oldTaxRate);
|
||||
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onTaxRateActivating` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onInactivating, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatingPayload);
|
||||
|
||||
const taxRate = await TaxRate.query(trx)
|
||||
.findById(taxRateId)
|
||||
.patch({ active: 0 });
|
||||
|
||||
// Triggers `onTaxRateCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.taxRates.onInactivated, {
|
||||
taxRateId,
|
||||
tenantId,
|
||||
trx,
|
||||
} as ITaxRateActivatedPayload);
|
||||
|
||||
return taxRate;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { keyBy, sumBy } from 'lodash';
|
||||
import { ItemEntry } from '@/models';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { IItem, IItemEntry, IItemEntryDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
export class ItemEntriesTaxTransactions {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Associates tax amount withheld to the model.
|
||||
* @param model
|
||||
* @returns
|
||||
*/
|
||||
public assocTaxAmountWithheldFromEntries(model: any) {
|
||||
const entries = model.entries.map((entry) => ItemEntry.fromJson(entry));
|
||||
const taxAmountWithheld = sumBy(entries, 'taxAmount');
|
||||
|
||||
if (taxAmountWithheld) {
|
||||
model.taxAmountWithheld = taxAmountWithheld;
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates tax rate id from tax code to entries.
|
||||
* @param {number} tenantId
|
||||
* @param {} model
|
||||
*/
|
||||
public assocTaxRateIdFromCodeToEntries =
|
||||
(tenantId: number) => async (entries: any) => {
|
||||
const entriesWithCode = entries.filter((entry) => entry.taxCode);
|
||||
const taxCodes = entriesWithCode.map((entry) => entry.taxCode);
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxCodes = await TaxRate.query().whereIn('code', taxCodes);
|
||||
|
||||
const taxCodesMap = keyBy(foundTaxCodes, 'code');
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (entry.taxCode) {
|
||||
entry.taxRateId = taxCodesMap[entry.taxCode]?.id;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Associates tax rate from tax id to entries.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<IItemEntry[]>}
|
||||
*/
|
||||
public assocTaxRateFromTaxIdToEntries =
|
||||
(tenantId: number) => async (entries: IItemEntry[]) => {
|
||||
const entriesWithId = entries.filter((e) => e.taxRateId);
|
||||
const taxRateIds = entriesWithId.map((e) => e.taxRateId);
|
||||
|
||||
const { TaxRate } = this.tenancy.models(tenantId);
|
||||
const foundTaxes = await TaxRate.query().whereIn('id', taxRateIds);
|
||||
|
||||
const taxRatesMap = keyBy(foundTaxes, 'id');
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (entry.taxRateId) {
|
||||
entry.taxRate = taxRatesMap[entry.taxRateId]?.rate;
|
||||
}
|
||||
return entry;
|
||||
});
|
||||
};
|
||||
}
|
||||
29
packages/server/src/services/TaxRates/TaxRateTransformer.ts
Normal file
29
packages/server/src/services/TaxRates/TaxRateTransformer.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class TaxRateTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to tax rate object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['nameFormatted', 'rateFormatted'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the formatted rate.
|
||||
* @param taxRate
|
||||
* @returns {string}
|
||||
*/
|
||||
public rateFormatted = (taxRate): string => {
|
||||
return `${taxRate.rate}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formats the tax rate name.
|
||||
* @param taxRate
|
||||
* @returns {string}
|
||||
*/
|
||||
protected nameFormatted = (taxRate): string => {
|
||||
return `${taxRate.name} [${taxRate.rate}%]`;
|
||||
};
|
||||
}
|
||||
109
packages/server/src/services/TaxRates/TaxRatesApplication.ts
Normal file
109
packages/server/src/services/TaxRates/TaxRatesApplication.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ICreateTaxRateDTO, IEditTaxRateDTO } from '@/interfaces';
|
||||
import { CreateTaxRate } from './CreateTaxRate';
|
||||
import { DeleteTaxRateService } from './DeleteTaxRate';
|
||||
import { EditTaxRateService } from './EditTaxRate';
|
||||
import { GetTaxRateService } from './GetTaxRate';
|
||||
import { GetTaxRatesService } from './GetTaxRates';
|
||||
import { ActivateTaxRateService } from './ActivateTaxRate';
|
||||
import { InactivateTaxRateService } from './InactivateTaxRate';
|
||||
|
||||
@Service()
|
||||
export class TaxRatesApplication {
|
||||
@Inject()
|
||||
private createTaxRateService: CreateTaxRate;
|
||||
|
||||
@Inject()
|
||||
private editTaxRateService: EditTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private deleteTaxRateService: DeleteTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private getTaxRateService: GetTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private getTaxRatesService: GetTaxRatesService;
|
||||
|
||||
@Inject()
|
||||
private activateTaxRateService: ActivateTaxRateService;
|
||||
|
||||
@Inject()
|
||||
private inactivateTaxRateService: InactivateTaxRateService;
|
||||
|
||||
/**
|
||||
* Creates a new tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreateTaxRateDTO} createTaxRateDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public createTaxRate(tenantId: number, createTaxRateDTO: ICreateTaxRateDTO) {
|
||||
return this.createTaxRateService.createTaxRate(tenantId, createTaxRateDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @param {IEditTaxRateDTO} taxRateEditDTO
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public editTaxRate(
|
||||
tenantId: number,
|
||||
taxRateId: number,
|
||||
editTaxRateDTO: IEditTaxRateDTO
|
||||
) {
|
||||
return this.editTaxRateService.editTaxRate(
|
||||
tenantId,
|
||||
taxRateId,
|
||||
editTaxRateDTO
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public deleteTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.deleteTaxRateService.deleteTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
* @returns {Promise<ITaxRate>}
|
||||
*/
|
||||
public getTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.getTaxRateService.getTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the tax rates list.
|
||||
* @param {number} tenantId
|
||||
* @returns {Promise<ITaxRate[]>}
|
||||
*/
|
||||
public getTaxRates(tenantId: number) {
|
||||
return this.getTaxRatesService.getTaxRates(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public activateTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.activateTaxRateService.activateTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inactivates the given tax rate.
|
||||
* @param {number} tenantId
|
||||
* @param {number} taxRateId
|
||||
*/
|
||||
public inactivateTaxRate(tenantId: number, taxRateId: number) {
|
||||
return this.inactivateTaxRateService.inactivateTaxRate(tenantId, taxRateId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { sumBy, chain, keyBy } from 'lodash';
|
||||
import { IItemEntry, ITaxTransaction } from '@/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class WriteTaxTransactionsItemEntries {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Writes the tax transactions from the given item entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
*/
|
||||
public async writeTaxTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[],
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRateTransaction, TaxRate } = this.tenancy.models(tenantId);
|
||||
const aggregatedEntries = this.aggregateItemEntriesByTaxCode(itemEntries);
|
||||
|
||||
const entriesTaxRateIds = aggregatedEntries.map((entry) => entry.taxRateId);
|
||||
|
||||
const taxRates = await TaxRate.query(trx).whereIn('id', entriesTaxRateIds);
|
||||
const taxRatesById = keyBy(taxRates, 'id');
|
||||
|
||||
const taxTransactions = aggregatedEntries.map((entry) => ({
|
||||
taxRateId: entry.taxRateId,
|
||||
referenceType: entry.referenceType,
|
||||
referenceId: entry.referenceId,
|
||||
rate: entry.taxRate || taxRatesById[entry.taxRateId]?.rate,
|
||||
})) as ITaxTransaction[];
|
||||
|
||||
await TaxRateTransaction.query(trx).upsertGraph(taxTransactions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites the tax rate transactions from the given item entries.
|
||||
* @param {number} tenantId
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
* @param {string} referenceType
|
||||
* @param {number} referenceId
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async rewriteTaxRateTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
itemEntries: IItemEntry[],
|
||||
referenceType: string,
|
||||
referenceId: number,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
await Promise.all([
|
||||
this.removeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
referenceId,
|
||||
referenceType,
|
||||
trx
|
||||
),
|
||||
this.writeTaxTransactionsFromItemEntries(tenantId, itemEntries, trx),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates by tax code id and sums the amount.
|
||||
* @param {IItemEntry[]} itemEntries
|
||||
* @returns {IItemEntry[]}
|
||||
*/
|
||||
private aggregateItemEntriesByTaxCode = (
|
||||
itemEntries: IItemEntry[]
|
||||
): IItemEntry[] => {
|
||||
return chain(itemEntries.filter((item) => item.taxRateId))
|
||||
.groupBy((item) => item.taxRateId)
|
||||
.values()
|
||||
.map((group) => ({ ...group[0], amount: sumBy(group, 'amount') }))
|
||||
.value();
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the tax transactions from the given item entries.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} referenceType - Reference type.
|
||||
* @param {number} referenceId - Reference id.
|
||||
*/
|
||||
public async removeTaxTransactionsFromItemEntries(
|
||||
tenantId: number,
|
||||
referenceId: number,
|
||||
referenceType: string,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { TaxRateTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
await TaxRateTransaction.query(trx)
|
||||
.where({ referenceType, referenceId })
|
||||
.delete();
|
||||
}
|
||||
}
|
||||
8
packages/server/src/services/TaxRates/constants.ts
Normal file
8
packages/server/src/services/TaxRates/constants.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const ERRORS = {
|
||||
TAX_RATE_NOT_FOUND: 'TAX_RATE_NOT_FOUND',
|
||||
TAX_CODE_NOT_UNIQUE: 'TAX_CODE_NOT_UNIQUE',
|
||||
ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_CODE_NOT_FOUND',
|
||||
ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND: 'ITEM_ENTRY_TAX_RATE_ID_NOT_FOUND',
|
||||
TAX_RATE_ALREADY_ACTIVE: 'TAX_RATE_ALREADY_ACTIVE',
|
||||
TAX_RATE_ALREADY_INACTIVE: 'TAX_RATE_ALREADY_INACTIVE'
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ISaleInvoiceCreatingPaylaod,
|
||||
ISaleInvoiceEditingPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { CommandTaxRatesValidators } from '../CommandTaxRatesValidators';
|
||||
|
||||
@Service()
|
||||
export class SaleInvoiceTaxRateValidateSubscriber {
|
||||
@Inject()
|
||||
private taxRateDTOValidator: CommandTaxRatesValidators;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreating,
|
||||
this.validateSaleInvoiceEntriesTaxIdExistanceOnCreating
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEditing,
|
||||
this.validateSaleInvoiceEntriesTaxCodeExistanceOnEditing
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEditing,
|
||||
this.validateSaleInvoiceEntriesTaxIdExistanceOnEditing
|
||||
);
|
||||
return bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnCreating = async ({
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatingPaylaod) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the tax rate id existance when creating.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxIdExistanceOnCreating = async ({
|
||||
saleInvoiceDTO,
|
||||
tenantId,
|
||||
}: ISaleInvoiceCreatingPaylaod) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate invoice entries tax rate code existance when editing.
|
||||
* @param {ISaleInvoiceEditingPayload}
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxCodeExistanceOnEditing = async ({
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
}: ISaleInvoiceEditingPayload) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCode(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the invoice entries tax rate id existance when editing.
|
||||
* @param {ISaleInvoiceEditingPayload} payload -
|
||||
*/
|
||||
private validateSaleInvoiceEntriesTaxIdExistanceOnEditing = async ({
|
||||
tenantId,
|
||||
saleInvoiceDTO,
|
||||
}: ISaleInvoiceEditingPayload) => {
|
||||
await this.taxRateDTOValidator.validateItemEntriesTaxCodeId(
|
||||
tenantId,
|
||||
saleInvoiceDTO.entries
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ISaleInvoiceCreatedPayload,
|
||||
ISaleInvoiceDeletedPayload,
|
||||
ISaleInvoiceEditedPayload,
|
||||
} from '@/interfaces';
|
||||
import events from '@/subscribers/events';
|
||||
import { WriteTaxTransactionsItemEntries } from '../WriteTaxTransactionsItemEntries';
|
||||
|
||||
@Service()
|
||||
export class WriteInvoiceTaxTransactionsSubscriber {
|
||||
@Inject()
|
||||
private writeTaxTransactions: WriteTaxTransactionsItemEntries;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach(bus) {
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onCreated,
|
||||
this.writeInvoiceTaxTransactionsOnCreated
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onEdited,
|
||||
this.rewriteInvoiceTaxTransactionsOnEdited
|
||||
);
|
||||
bus.subscribe(
|
||||
events.saleInvoice.onDelete,
|
||||
this.removeInvoiceTaxTransactionsOnDeleted
|
||||
);
|
||||
return bus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the invoice tax transactions on invoice created.
|
||||
* @param {ISaleInvoiceCreatingPaylaod}
|
||||
*/
|
||||
private writeInvoiceTaxTransactionsOnCreated = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx
|
||||
}: ISaleInvoiceCreatedPayload) => {
|
||||
await this.writeTaxTransactions.writeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
saleInvoice.entries,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Rewrites the invoice tax transactions on invoice edited.
|
||||
* @param {ISaleInvoiceEditedPayload} payload -
|
||||
*/
|
||||
private rewriteInvoiceTaxTransactionsOnEdited = async ({
|
||||
tenantId,
|
||||
saleInvoice,
|
||||
trx,
|
||||
}: ISaleInvoiceEditedPayload) => {
|
||||
await this.writeTaxTransactions.rewriteTaxRateTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
saleInvoice.entries,
|
||||
'SaleInvoice',
|
||||
saleInvoice.id,
|
||||
trx
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes the invoice tax transactions on invoice deleted.
|
||||
* @param {ISaleInvoiceEditingPayload}
|
||||
*/
|
||||
private removeInvoiceTaxTransactionsOnDeleted = async ({
|
||||
tenantId,
|
||||
oldSaleInvoice,
|
||||
trx
|
||||
}: ISaleInvoiceDeletedPayload) => {
|
||||
await this.writeTaxTransactions.removeTaxTransactionsFromItemEntries(
|
||||
tenantId,
|
||||
oldSaleInvoice.id,
|
||||
'SaleInvoice',
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,7 @@ export default {
|
||||
sendResetPassword: 'onSendResetPassword',
|
||||
|
||||
resetPassword: 'onResetPassword',
|
||||
resetingPassword: 'onResetingPassword'
|
||||
resetingPassword: 'onResetingPassword',
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -560,4 +560,21 @@ export default {
|
||||
onDeleting: 'onProjectTimeDeleting',
|
||||
onDeleted: 'onProjectTimeDeleted',
|
||||
},
|
||||
|
||||
taxRates: {
|
||||
onCreating: 'onTaxRateCreating',
|
||||
onCreated: 'onTaxRateCreated',
|
||||
|
||||
onEditing: 'onTaxRateEditing',
|
||||
onEdited: 'onTaxRateEdited',
|
||||
|
||||
onDeleting: 'onTaxRateDeleting',
|
||||
onDeleted: 'onTaxRateDeleted',
|
||||
|
||||
onActivating: 'onTaxRateActivating',
|
||||
onActivated: 'onTaxRateActivated',
|
||||
|
||||
onInactivating: 'onTaxRateInactivating',
|
||||
onInactivated: 'onTaxRateInactivated'
|
||||
},
|
||||
};
|
||||
|
||||
@@ -471,6 +471,15 @@ const castCommaListEnvVarToArray = (envVar: string): Array<string> => {
|
||||
return envVar ? envVar?.split(',')?.map(_.trim) : [];
|
||||
};
|
||||
|
||||
export const sortObjectKeysAlphabetically = (object) => {
|
||||
return Object.keys(object)
|
||||
.sort()
|
||||
.reduce((objEntries, key) => {
|
||||
objEntries[key] = object[key];
|
||||
return objEntries;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export {
|
||||
templateRender,
|
||||
accumSum,
|
||||
@@ -503,5 +512,5 @@ export {
|
||||
mergeObjectsBykey,
|
||||
nestedArrayToFlatten,
|
||||
assocDepthLevelToObjectTree,
|
||||
castCommaListEnvVarToArray
|
||||
castCommaListEnvVarToArray,
|
||||
};
|
||||
|
||||
19
packages/server/src/utils/taxRate.ts
Normal file
19
packages/server/src/utils/taxRate.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Get inclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getInclusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / (100 + taxRate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get exclusive tax amount.
|
||||
* @param {number} amount
|
||||
* @param {number} taxRate
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getExlusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||
return (amount * taxRate) / 100;
|
||||
};
|
||||
13
packages/webapp/package-lock.json
generated
13
packages/webapp/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -20,7 +20,8 @@ export const AbilitySubject = {
|
||||
SubscriptionBilling: 'SubscriptionBilling',
|
||||
CreditNote: 'CreditNote',
|
||||
VendorCredit: 'VendorCredit',
|
||||
Project:'Project'
|
||||
Project:'Project',
|
||||
TaxRate: 'TaxRate',
|
||||
};
|
||||
|
||||
export const ItemAction = {
|
||||
@@ -169,6 +170,7 @@ export const ReportsAction = {
|
||||
READ_INVENTORY_VALUATION_SUMMARY: 'read-inventory-valuation-summary',
|
||||
READ_INVENTORY_ITEM_DETAILS: 'read-inventory-item-details',
|
||||
READ_CASHFLOW_ACCOUNT_TRANSACTION: 'read-cashflow-account-transactions',
|
||||
READ_SALES_TAX_LIABILITY_SUMMARY: 'read-sales-tax-liability-summary',
|
||||
};
|
||||
|
||||
export const PreferencesAbility = {
|
||||
@@ -185,3 +187,11 @@ export const SubscriptionBillingAbility = {
|
||||
View: 'view',
|
||||
Payment: 'payment',
|
||||
};
|
||||
|
||||
|
||||
export const TaxRateAction = {
|
||||
View: 'View',
|
||||
Create: 'Create',
|
||||
Edit: 'Edit',
|
||||
Delete: 'Delete',
|
||||
};
|
||||
|
||||
@@ -46,5 +46,6 @@ export enum DialogsName {
|
||||
EstimateExpenseForm = 'estimate-expense-form',
|
||||
ProjectInvoicingForm = 'project-invoicing-form',
|
||||
ProjectBillableEntriesForm = 'project-billable-entries',
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings'
|
||||
InvoiceNumberSettings = 'InvoiceNumberSettings',
|
||||
TaxRateForm = 'tax-rate-form',
|
||||
}
|
||||
|
||||
@@ -22,4 +22,5 @@ export enum DRAWERS {
|
||||
REFUND_CREDIT_NOTE_DETAILS = 'refund-credit-detail-drawer',
|
||||
REFUND_VENDOR_CREDIT_DETAILS = 'refund-vendor-detail-drawer',
|
||||
WAREHOUSE_TRANSFER_DETAILS = 'warehouse-transfer-detail-drawer',
|
||||
TAX_RATE_DETAILS = 'tax-rate-detail-drawer',
|
||||
}
|
||||
|
||||
@@ -87,9 +87,6 @@ export const financialReportMenus = [
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const SalesAndPurchasesReportMenus = [
|
||||
{
|
||||
sectionTitle: <T id={'sales_purchases_reports'} />,
|
||||
reports: [
|
||||
@@ -119,19 +116,6 @@ export const SalesAndPurchasesReportMenus = [
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_BY_ITEMS,
|
||||
},
|
||||
{
|
||||
title: <T id={'inventory_valuation'} />,
|
||||
desc: (
|
||||
<T
|
||||
id={
|
||||
'summarize_the_business_s_purchase_items_quantity_cost_and_average'
|
||||
}
|
||||
/>
|
||||
),
|
||||
link: '/financial-reports/inventory-valuation',
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
|
||||
},
|
||||
{
|
||||
title: <T id={'customers_balance_summary'} />,
|
||||
desc: (
|
||||
@@ -189,4 +173,16 @@ export const SalesAndPurchasesReportMenus = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
sectionTitle: 'Taxes',
|
||||
reports: [
|
||||
{
|
||||
title: 'Sales Tax Liability Summary',
|
||||
desc: 'Reports the total amount of sales tax collected from customers',
|
||||
link: '/financial-reports/sales-tax-liability-summary',
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ExpenseAction,
|
||||
CashflowAction,
|
||||
PreferencesAbility,
|
||||
TaxRateAction,
|
||||
} from '@/constants/abilityOption';
|
||||
import { DialogsName } from './dialogs';
|
||||
|
||||
@@ -406,6 +407,15 @@ export const SidebarMenu = [
|
||||
href: '/transactions-locking',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
},
|
||||
{
|
||||
text: 'Tax Rates',
|
||||
href: '/tax-rates',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
permission: {
|
||||
subject: AbilitySubject.TaxRate,
|
||||
ability: TaxRateAction.View,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -741,6 +751,21 @@ export const SidebarMenu = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: 'Taxes',
|
||||
type: ISidebarMenuItemType.Group,
|
||||
children: [
|
||||
{
|
||||
text: 'Sales Tax Liability Summary',
|
||||
href: '/financial-reports/sales-tax-liability-summary',
|
||||
type: ISidebarMenuItemType.Link,
|
||||
permission: {
|
||||
subject: AbilitySubject.Report,
|
||||
ability: ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: <T id={'sidebar.inventory'} />,
|
||||
type: ISidebarMenuItemType.Group,
|
||||
|
||||
@@ -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
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@ const ARAgingSummaryHeaderDimensonsContext = React.createContext();
|
||||
|
||||
/**
|
||||
* ARAging summary header dismensions provider.
|
||||
* @returns
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
function ARAgingSummaryHeaderDimensionsProvider({ query, ...props }) {
|
||||
// Features guard.
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function CashFlowStatementTable({
|
||||
expandable={true}
|
||||
expanded={expandedRows}
|
||||
expandToggleColumn={1}
|
||||
sticky={true}
|
||||
expandColumnSpace={0.8}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ export default function CustomersBalanceSummaryTable({
|
||||
data={table.data}
|
||||
rowClassNames={tableRowTypesToClassnames}
|
||||
noInitialFetch={true}
|
||||
sticky={true}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
</FinancialSheet>
|
||||
|
||||
@@ -47,6 +47,7 @@ export default function CustomersTransactionsTable({
|
||||
noInitialFetch={true}
|
||||
expandable={true}
|
||||
expanded={expandedRows}
|
||||
sticky={true}
|
||||
expandToggleColumn={1}
|
||||
expandColumnSpace={0.8}
|
||||
styleName={TableStyle.Constrant}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ export function InventoryItemDetailsTable({
|
||||
expanded={expandedRows}
|
||||
expandToggleColumn={1}
|
||||
expandColumnSpace={0.8}
|
||||
sticky={true}
|
||||
styleName={TableStyle.Constrant}
|
||||
/>
|
||||
</FinancialSheet>
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
`;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 />;
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user